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.
This commit is contained in:
Soheil Fakour 2026-08-03 10:41:08 -04:00 committed by kshitij
parent 5444f6853b
commit e9d1551e65
2 changed files with 34 additions and 1 deletions

View File

@ -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:

View File

@ -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)"