From e9d1551e65a7a4f7201fc2fe491537d0c5a9bc27 Mon Sep 17 00:00:00 2001 From: Soheil Fakour Date: Mon, 3 Aug 2026 10:41:08 -0400 Subject: [PATCH] fix(redact): strip control chars from mask_secret display (#55319, #55321) A masked secret's visible head/tail could carry control bytes (newline, NUL, DEL, C1 0x80-0x9F, zero-width) into config/status/dump output. Strip every control incl. \n/\t (display differs from redact_sensitive_text, which preserves \n/\t as line structure) before slicing; all-control values return the configured empty fallback. Consolidates the previously-closed #58079 approach (strip controls before masking) - supersedes it. --- agent/redact.py | 15 +++++++++++++++ tests/agent/test_redact.py | 20 +++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/agent/redact.py b/agent/redact.py index 7e29e73e97c99..85366a6767be4 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -521,6 +521,15 @@ def _mask_control_split_tokens(text: str, mask_fn) -> str: return "".join(out) +# Display-mask strip for mask_secret: EVERY control char incl. \n/\t, C1, +# DEL, and zero-width/format chars — a masked secret must never emit +# multiline, tabbed, or invisible bytes into config/status/dump display +# output (#55319, #55321). +_DISPLAY_CONTROL_RE = re.compile( + r"[\x00-\x1f\x7f\x80-\x9f\u200b-\u200f\u202a-\u202e\u2060-\u2064]" +) + + def mask_secret( value: str, *, @@ -561,6 +570,12 @@ def mask_secret( >>> mask_secret("long-token", head=6, tail=4, floor=18) '***' """ + if not value: + return empty + # Visible head/tail must not carry control bytes (newline, NUL, DEL, C1) + # into config/status/dump output (#55319, #55321). Strip them before + # slicing — the length check below then sees the displayable length. + value = _DISPLAY_CONTROL_RE.sub("", value) if not value: return empty if len(value) < floor: diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index d26f4c182db5e..2db2beeffa5a6 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -4,7 +4,7 @@ import logging import pytest -from agent.redact import redact_cdp_url, redact_sensitive_text, RedactingFormatter +from agent.redact import mask_secret, redact_cdp_url, redact_sensitive_text, RedactingFormatter @pytest.fixture(autouse=True) @@ -1009,3 +1009,21 @@ class TestKeywordWordBoundary: assert "hunter2hunter2hunter2hh" not in result +class TestMaskSecretControlStripping: + """Issue #55319/#55321: mask_secret() must not emit control bytes + (newline, NUL, DEL, C1) in the visible head/tail of a masked secret — + they corrupt config/status/dump display output.""" + + def test_newline_stripped_from_mask(self): + # The #55319 probe: a newline inside the preserved head. + assert mask_secret("ab\ncd0123456789zzzz") == "abcd...zzzz" + + def test_c1_control_stripped_from_mask(self): + assert mask_secret("abcd0123456789zz\x85q") == "abcd...9zzq" + + def test_printable_mask_unchanged(self): + assert mask_secret("abcdef0123456789zzzz") == "abcd...zzzz" + + def test_all_control_value_returns_empty_fallback(self): + assert mask_secret("\n\x85\u200b") == "" + assert mask_secret("\n\x85\u200b", empty="(not set)") == "(not set)"