Skip to content

fix(redact): strip complete CSI/SGR sequences in token-split shadow (#81012) - #81060

Closed
Enough1122 wants to merge 1 commit into
NousResearch:mainfrom
Enough1122:fix/81012-redact-csi-sgr-join
Closed

fix(redact): strip complete CSI/SGR sequences in token-split shadow (#81012)#81060
Enough1122 wants to merge 1 commit into
NousResearch:mainfrom
Enough1122:fix/81012-redact-csi-sgr-join

Conversation

@Enough1122

Copy link
Copy Markdown
Contributor

Fixes #81012

_mask_control_split_tokens stripped only the bare ESC byte, leaving
the CSI parameter bytes ([32m etc.) glued to the token head. The
literal m (or any other letter) defeated _PREFIX_RE's
(?<![A-Za-z0-9_-]) lookbehind, so neither the ordinary prefix pass
nor the split-join pass matched and the secret leaked verbatim — common
when logs are colored with \x1b[32m or a terminal adds a SGR
prefix around a tool output line.

This PR:

  • Adds _CSI_SEQUENCE_RE matching the SGR-relevant CSI subset
    (\x1b\[[0-9;?]*[ -/]*[@-~]).
  • Builds the shadow copy with _CSI_SEQUENCE_RE and
    _CONTROL_CHARS_RE applied in that order (CSI matches first, so we
    never delete just the ESC byte and leave the parameter bytes glued to
    adjacent text).
  • Re-derives orig_idx from text directly, skipping both CSI
    sequences and bare control chars in one pass.
  • Updates the maskable-span check to also strip CSI from the span, so
    CSI parameter bytes inside a span don't get rejected as "unknown
    non-token chars".

Regression tests:

  • tests/agent/test_redact.py::test_complete_csi_sgr_glued_to_token_masked
    \x1b[32m sk-… \x1b[0m (the colored-log header case).
  • tests/agent/test_redact.py::test_csi_between_token_fragments_joins
    sk-<head>\x1b[32m<tail>, the bare-ESC strip used to defeat the join.
  • tests/agent/test_redact.py::test_csi_within_prose_does_not_match
    CSI in prose with no token must still pass through unchanged.

…ousResearch#81012)

`_mask_control_split_tokens` stripped only the bare ESC byte, leaving
the CSI parameter bytes (`[32m` etc.) glued to the token head. The
literal `m` defeated `_PREFIX_RE`'s `(?<![A-Za-z0-9_-])` lookbehind
so neither the ordinary prefix pass nor the split-join pass matched and
the secret (`sk-…`, `ghp_…`, etc.) leaked verbatim in colored log
output.

Strip complete CSI sequences (`\x1b[...letter`) from the shadow copy
alongside the existing bare control char strip, and update the
maskable-span check to also strip CSI so CSI parameter bytes inside a
span don't get rejected as `unknown non-token chars`.

Regression tests cover:
* `\x1b[32m sk-… \x1b[0m` — the colored-log header case,
* `sk-<head>\x1b[32m<tail>` — CSI between fragments (the bare-ESC
  strip used to defeat the join),
* `\x1b[1;32mWelcome to the dashboard\x1b[0m` — CSI in prose with no
  token must still pass through unchanged.
@Enough1122

Copy link
Copy Markdown
Contributor Author

Collision note (from a full open-PR scan): #81121 (Parker-Fawcett, opened later) addresses the same issue #81012 with the same approach in the same files (agent/redact.py, tests/agent/test_redact.py — strip complete CSI/SGR sequences). This PR opened first (13:38 vs 15:08) with a near-identical regex approach; #81121 is the duplicate. Happy to rebase or adjust if maintainers prefer its variant.

@Enough1122

Copy link
Copy Markdown
Contributor Author

Closing in favor of #81121 per maintainer @andrexibiza's routing decision on issue #81012: the five-head matrix test showed #81121 as the only implementation covering both the CSI-glue leak and mixed ESC+newline cases, with this PR classified as partial coverage and the explicit instruction to 'route the fix through #81121'. Happy to contribute this branch's CSI regression tests to #81121 if useful. Branch left for manual cleanup.

@Enough1122 Enough1122 closed this Aug 10, 2026
@andrexibiza

Copy link
Copy Markdown
Contributor

Closing in favor of #81121 per maintainer @andrexibiza's routing decision on issue #81012: the five-head matrix test showed #81121 as the only implementation covering both the CSI-glue leak and mixed ESC+newline cases, with this PR classified as partial coverage and the explicit instruction to 'route the fix through #81121'. Happy to contribute this branch's CSI regression tests to #81121 if useful. Branch left for manual cleanup.

I'll never say no to more regression tests. Thank you for your work!

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

redact: complete CSI/SGR sequences defeat prefix masking (ESC-byte-only stripping leaves 'm'-glue)

3 participants