Skip to content

fix(sdk): prevent over-redaction of dict keys with keyword substrings - #4581

Draft
all-hands-bot wants to merge 1 commit into
mainfrom
fix/redact-over-matching-keyword-substrings
Draft

fix(sdk): prevent over-redaction of dict keys with keyword substrings#4581
all-hands-bot wants to merge 1 commit into
mainfrom
fix/redact-over-matching-keyword-substrings

Conversation

@all-hands-bot

@all-hands-bot all-hands-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

HUMAN:


AGENT:

Why

Source PR reviewed: #4508 (case-insensitive dict-entry secret redaction in redact.py)

Applicable risk lens: #10 (Negative-space and silent-success cases) — the PR
introduced over-redaction that silently returns success with incorrect output.

PR #4508 added re.IGNORECASE to the dict-entry secret redaction regexes in
redact_text_secrets(). The pattern [A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*
with IGNORECASE matches any key containing those keywords as bare substrings,
not just underscore/camelCase-separated components. This causes non-sensitive keys
like monkey, donkey, keyboard, secretary, keyword, and tokenizer to
have their values incorrectly replaced with <redacted>.

Deterministic reproduction and impact:

from openhands.sdk.utils.redact import redact_text_secrets
text = "{'monkey': 'banana', 'secretary': 'Jane Doe', 'tokenizer': 'bert'}"
print(redact_text_secrets(text))
# BEFORE FIX:  {'monkey': '<redacted>', 'secretary': '<redacted>', 'tokenizer': '<redacted>'}
# AFTER FIX:   {'monkey': 'banana', 'secretary': 'Jane Doe', 'tokenizer': 'bert'}

This over-redaction corrupts log output and telemetry, making non-sensitive data
unreadable and potentially causing debugging confusion when values that are
explicitly NOT secret get masked.

Root cause: The character class [A-Z_] with re.IGNORECASE matches all
letters (upper and lower). So [A-Z_]*KEY[A-Z_]* matches monkey because mon
matches [A-Z_]* and key matches KEY (case-insensitive). The pre-#4508
case-sensitive pattern only matched uppercase compound names like API_KEY.

Why the original tests did not catch the case: The tests in #4508 verified that
lowercase sensitive keys (api_key, token) ARE redacted — which is correct — but
did not test that non-sensitive keys containing keyword substrings (monkey,
secretary) are NOT redacted. The test for non-sensitive keys only used name
and path, neither of which contains a keyword substring.

Proof that the regression test fails on unmodified main:

$ uv run pytest tests/sdk/utils/test_redact.py::TestRedactTextSecretsDictKeys::test_does_not_over_redact_keys_containing_keyword_substrings -xvs

FAILED tests/sdk/utils/test_redact.py::TestRedactTextSecretsDictKeys::test_does_not_over_redact_keys_containing_keyword_substrings
assert "{'monkey': '<redacted>', 'donkey': '<redacted>', ...}" == "{'monkey': 'banana', 'donkey': 'farm', ...}"

Summary

  • Replace the single [A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)[A-Z_]* pattern (with
    re.IGNORECASE) with two alternations: an underscore-delimited pattern
    (case-insensitive, matching api_key, MY_SECRET, key) and a
    PascalCase/camelCase pattern (case-sensitive, matching UserPassword, apiKey).
  • Add a regression test verifying that keys like monkey, donkey, keyboard,
    secretary, keyword, and tokenizer are NOT redacted.

Issue Number

N/A — found via weekly regression hunt.

How to Test

Before fix (fails on main):

uv run pytest tests/sdk/utils/test_redact.py::TestRedactTextSecretsDictKeys::test_does_not_over_redact_keys_containing_keyword_substrings -xvs
# FAILED — monkey/donkey/keyboard/secretary/keyword/tokenizer values are over-redacted

After fix (passes):

uv run pytest tests/sdk/utils/test_redact.py -xvs
# 39 passed

Full edge-case probe (run on main baseline with fix applied):

from openhands.sdk.utils.redact import redact_text_secrets
# Non-sensitive keys (should NOT be redacted):
assert redact_text_secrets("{'monkey': 'banana'}") == "{'monkey': 'banana'}"
assert redact_text_secrets("{'secretary': 'Jane'}") == "{'secretary': 'Jane'}"
assert redact_text_secrets("{'tokenizer': 'bert'}") == "{'tokenizer': 'bert'}"
# Sensitive keys (SHOULD be redacted):
assert "<redacted>" in redact_text_secrets("{'api_key': 'sk-abc'}")
assert "<redacted>" in redact_text_secrets("{'UserPassword': 'p@ss'}")
assert "<redacted>" in redact_text_secrets("{'apiKey': 'sk-abc'}")
assert "<redacted>" in redact_text_secrets("{'MY_SECRET': 'abc'}")
# All 21 edge cases pass — see commit for full list.

End-to-end validation:

$ uv run pytest tests/sdk/utils/test_redact.py -v
# 39 passed in 0.09s

Video/Screenshots

N/A — text-based redaction logic; reproduction output included above.

Design Doc

N/A — minimal regex fix.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:986ede8-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-986ede8-python \
  ghcr.io/openhands/agent-server:986ede8-python

All tags pushed for this build

ghcr.io/openhands/agent-server:986ede8-golang-amd64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-golang-amd64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-golang-amd64
ghcr.io/openhands/agent-server:986ede8-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:986ede8-golang-arm64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-golang-arm64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-golang-arm64
ghcr.io/openhands/agent-server:986ede8-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:986ede8-java-amd64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-java-amd64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-java-amd64
ghcr.io/openhands/agent-server:986ede8-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:986ede8-java-arm64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-java-arm64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-java-arm64
ghcr.io/openhands/agent-server:986ede8-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:986ede8-python-amd64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-python-amd64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-python-amd64
ghcr.io/openhands/agent-server:986ede8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:986ede8-python-arm64
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-python-arm64
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-python-arm64
ghcr.io/openhands/agent-server:986ede8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:986ede8-golang
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-golang
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-golang
ghcr.io/openhands/agent-server:986ede8-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:986ede8-java
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-java
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-java
ghcr.io/openhands/agent-server:986ede8-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:986ede8-python
ghcr.io/openhands/agent-server:986ede8c6811155205980d24df154efdbf2e2bc0-python
ghcr.io/openhands/agent-server:fix-redact-over-matching-keyword-substrings-python
ghcr.io/openhands/agent-server:986ede8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 986ede8-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 986ede8-python-amd64) are also available if needed

PR #4508 added re.IGNORECASE to the dict-entry redaction regex, which
caused non-sensitive keys containing KEY/SECRET/TOKEN/PASSWORD as
substrings (e.g. monkey, donkey, keyboard, secretary, tokenizer) to
have their values incorrectly redacted.

The fix splits the pattern into two alternations:
- Underscore-delimited components (case-insensitive): api_key, MY_SECRET
- PascalCase/camelCase components (case-sensitive): UserPassword, apiKey

This preserves the intent of #4508 (matching lowercase api_key, token)
while avoiding false positives on common English words.

Co-authored-by: openhands <openhands@all-hands.dev>
@all-hands-bot
all-hands-bot requested a review from enyst August 23, 2026 00:44
@github-actions

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/utils
   redact.py921485%87, 230–231, 254–260, 276–279
TOTAL406011128672% 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants