Skip to content
Merged
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
4 changes: 3 additions & 1 deletion openhands-sdk/openhands/sdk/utils/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,18 @@ 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
# Dict entries with sensitive key names (case-insensitive, like is_secret_key)
text = re.sub(
r"('[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*':\s*')[^']*(')",
r"\g<1><redacted>\2",
text,
flags=re.IGNORECASE,
)
text = re.sub(
r'("[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*":\s*")[^"]*(")',
r"\g<1><redacted>\2",
text,
flags=re.IGNORECASE,
)

# URL query params
Expand Down
33 changes: 33 additions & 0 deletions tests/sdk/utils/test_redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from openhands.sdk.utils.redact import (
SENSITIVE_URL_PARAMS,
redact_text_secrets,
redact_url_credentials,
redact_url_credentials_in_text,
redact_url_params,
Expand Down Expand Up @@ -251,3 +252,35 @@ def test_matches_whole_url_like_anchored_helper(self):
"""For a bare whole-URL string both helpers agree."""
url = "https://oauth2:SECRET@gitlab.com/org/repo.git"
assert redact_url_credentials_in_text(url) == redact_url_credentials(url)


# ---------------------------------------------------------------------------
# redact_text_secrets
# ---------------------------------------------------------------------------


class TestRedactTextSecretsDictKeys:
"""Dict-entry redaction is case-insensitive, like is_secret_key.

Regression tests for https://github.com/OpenHands/software-agent-sdk/issues/4505
"""

def test_redacts_lowercase_and_mixed_case_dict_keys(self):
text = (
"{'api_key': 's3cr3t', \"token\": \"tok-XYZ\", "
"'UserPassword': 'p@ssw0rd', 'normal': 'keep-me'}"
)
assert redact_text_secrets(text) == (
"{'api_key': '<redacted>', \"token\": \"<redacted>\", "
"'UserPassword': '<redacted>', 'normal': 'keep-me'}"
)

def test_uppercase_dict_keys_still_redacted(self):
text = "{'API_KEY': 'abc', \"MY_SECRET\": \"def\"}"
assert redact_text_secrets(text) == (
"{'API_KEY': '<redacted>', \"MY_SECRET\": \"<redacted>\"}"
)

def test_non_sensitive_entries_unchanged(self):
text = "{'name': 'alice', 'path': '/tmp/x'}"
assert redact_text_secrets(text) == text
Loading