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:
parent
5444f6853b
commit
e9d1551e65
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
Loading…
Reference in New Issue