Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions openhands-sdk/openhands/sdk/utils/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,18 +302,25 @@ def redact_text_secrets(text: str) -> str:
text = re.sub(r"api_key='[^']*'", "api_key='<redacted>'", text)
text = re.sub(r'api_key="[^"]*"', 'api_key="<redacted>"', text)

# Dict entries with sensitive key names (case-insensitive, like is_secret_key)
# Dict entries with sensitive key names (case-insensitive, like is_secret_key).
# The keyword must be an underscore-separated component of the key name
# (e.g. api_key, MY_SECRET) or a PascalCase/camelCase component (e.g.
# UserPassword, apiKey). A bare substring match is NOT enough — otherwise
# keys like "monkey", "secretary", "keyboard", or "tokenizer" would be
# over-redacted.
_dict_key_single = (
r"(?i:(?:[A-Za-z0-9]+_)*(?:KEY|SECRET|TOKEN|PASSWORD)(?:_[A-Za-z0-9]+)*)"
)
_dict_key_camel = r"[A-Za-z]*?(?:Key|Secret|Token|Password)[A-Za-z]*"
text = re.sub(
r"('[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*':\s*')[^']*(')",
rf"('(?:{_dict_key_single}|{_dict_key_camel})':\s*')[^']*(')",
r"\g<1><redacted>\2",
text,
flags=re.IGNORECASE,
)
text = re.sub(
r'("[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*":\s*")[^"]*(")',
rf'("(?:{_dict_key_single}|{_dict_key_camel})":\s*")[^"]*(")',
r"\g<1><redacted>\2",
text,
flags=re.IGNORECASE,
)

# URL query params
Expand Down
14 changes: 14 additions & 0 deletions tests/sdk/utils/test_redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,17 @@ def test_uppercase_dict_keys_still_redacted(self):
def test_non_sensitive_entries_unchanged(self):
text = "{'name': 'alice', 'path': '/tmp/x'}"
assert redact_text_secrets(text) == text

def test_does_not_over_redact_keys_containing_keyword_substrings(self):
"""Keys that merely contain a sensitive keyword as a substring
(without underscore separation) must not be over-redacted.

Regression: ``re.IGNORECASE`` on ``[A-Z_]*KEY[A-Z_]*`` matches
``monkey``, ``donkey``, ``keyboard``, ``secretary``, etc.
"""
text = (
"{'monkey': 'banana', 'donkey': 'farm', "
"'keyboard': 'mechanical', 'secretary': 'Jane Doe', "
"'keyword': 'search-term', 'tokenizer': 'bert-base'}"
)
assert redact_text_secrets(text) == text
Loading