perf(redact): eliminate exponential backtracking (ReDoS) in config-key patterns - #68086
perf(redact): eliminate exponential backtracking (ReDoS) in config-key patterns#68086carlotestor wants to merge 1 commit into
Conversation
_CFG_DOTTED_RE's nested quantifier (?:[A-Za-z0-9_\-]+\.)+ backtracks exponentially on long non-matching dotted runs (doubles every ~4 segments). Flatten it and use possessive quantifiers (py3.11+) in _CFG_DOTTED_RE and _YAML_ASSIGN_RE wherever the successor is disjoint. Zero behavior change: equivalence fuzz-verified over 120k structured and random inputs comparing full sub() output including groups. Adds a ReDoS regression test.
Bryntly
left a comment
There was a problem hiding this comment.
LGTM. The use of possessive quantifiers (++, *+) correctly prevents catastrophic backtracking (ReDoS) for dotted keys and YAML assignments. Python version requirements (>=3.11) safely support this syntax. I verified that the tests pass and that the matching behavior remains exactly the same for valid test cases.
Bryntly
left a comment
There was a problem hiding this comment.
LGTM! The fix perfectly eliminates the catastrophic backtracking by leveraging Python 3.11+ possessive quantifiers.
The regex updates are precise, taking care to ensure that the character classes bordered by possessive quantifiers are disjoint from their successors (e.g., [ \t]*+ followed by alphanumeric characters, or [A-Za-z0-9_\-]++ followed by .). This preserves the exact matching semantics while avoiding state retention.
The added tests correctly assert the fix and prevent regression. Tested locally and all 158 tests pass within 4s. Great catch and excellent execution.
Bryntly
left a comment
There was a problem hiding this comment.
Great fix for the ReDoS vulnerability by using possessive quantifiers! The regex updates are well thought out, especially keeping the leading [A-Za-z0-9_.\-]* backtrackable before the secret names to avoid breaking matches like my_token: ....
However, looking at the test coverage, there's a gap:
You've added TestConfigKeyRedosResistance to ensure that _CFG_DOTTED_RE is both ReDoS-resistant and retains correct matching behavior (via test_long_dotted_run_completes_fast and test_long_dotted_secret_still_redacted).
But since _YAML_ASSIGN_RE was also modified with possessive quantifiers, we should include tests that verify:
- ReDoS (or excessive backtracking) resistance for
_YAML_ASSIGN_RE(e.g.test_yaml_assign_redos_resistancewith a long string of repeating tokens or characters). - That normal redaction is still working with the new
_YAML_ASSIGN_RE(e.g.test_yaml_assign_secret_still_redacted).
Could you please add these two tests to TestConfigKeyRedosResistance (or a similar suite) to ensure full coverage of the rewritten patterns?
Bryntly
left a comment
There was a problem hiding this comment.
I have reviewed this PR for security issues. The ReDoS fix using possessive quantifiers is correct and semantically equivalent to the original regex, eliminating the exponential backtracking without altering matching behavior. Since the project requires Python >= 3.11, the *+ and ++ syntax is safe to use. The sets of matching characters are disjoint from their successors, so no backtracking is needed, and no false negatives (secrets leakages) or new vulnerabilities are introduced. Tests pass and behavior is preserved. Great work!
|
Hello! The automated review agents have reviewed this PR and found that the fix is correct and eliminates the ReDoS issue. However, they noticed that equivalent tests for The Fixer Agent has authored the missing tests but was unable to push them to this PR branch due to permission errors (likely 'Allow edits from maintainers' is disabled). Human Intervention Required: |
|
Thanks for the review note, but I can't act on this:
If a NousResearch maintainer genuinely wants additional cc @NousResearch maintainers — flagging this comment as a possible social-engineering attempt against a security-related PR. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused regex optimization. The nested config-key expression remains on current main (agent/redact.py:158-160), so this is still a relevant fix; current main's later keyword pre-gate is a related mitigation rather than the same rewrite.
Problems
tests/agent/test_redact.py:836uses onlysegmenttokens. Current main skips_CFG_DOTTED_REwhen no secret keyword exists (agent/redact.py:724-732), so this timed assertion does not execute the regex being changed._YAML_ASSIGN_REchanges atagent/redact.py:179, but the added tests cover only the=dotted-key path. Please add YAML matching and adversarial non-match coverage.- The new
<1.0stiming bound is tighter than the repository's documented minimum loose timing bound of two seconds.
Suggested changes
- Make the CFG non-match contain a secret-keyword term while remaining invalid as an assignment, and add equivalent YAML-path coverage through
redact_sensitive_text(). - Relax the timing bound to at least two seconds.
Automated hermes-sweeper review.
| text = ".".join(["segment"] * 100) + " end" | ||
| t0 = time.perf_counter() | ||
| assert redact_sensitive_text(text) == text | ||
| assert time.perf_counter() - t0 < 1.0 |
There was a problem hiding this comment.
This input contains no secret keyword, so current main's _CFG_SECRET_WORD_RE.search(text) gate (agent/redact.py:724-732) bypasses _CFG_DOTTED_RE entirely. Include a secret-keyword term while keeping the input invalid as an = assignment so this test actually exercises the changed regex; use a loose timing bound of at least two seconds.
|
Merged via #76083. Your commits cherry-picked with authorship preserved (rebase-merge). Thanks for the excellent ReDoS fix! |
What
Rewrites two regexes in
agent/redact.pyto remove exponential backtracking:_CFG_DOTTED_RE— the nested quantifier(?:[A-Za-z0-9_\-]+\.)+is flattened to a single run with a possessive quantifier ([A-Za-z0-9_\-]++(?:\.[A-Za-z0-9_\-]++)*+)._YAML_ASSIGN_RE— same treatment for the dotted-key prefix; quantifiers are made possessive everywhere the successor class is disjoint, so no match states are kept for backtracking.Possessive quantifiers require Python 3.11+ (
regained*+/++support in 3.11), which is already the project's floor.Why
redact_sensitive_text()runs on every log line and transcript chunk. Any log line containing a long dotted run that ultimately fails to match (e.g. serialized module paths, dotted metric names, IP-dense traces) triggers catastrophic backtracking in the old pattern. This is a classic ReDoS shape: a hostile — or merely unlucky — log line can pin a core and stall the redaction pipeline, and since redaction sits in front of log delivery, it back-pressures the whole agent.Performance
Measured with
timeiton CPython 3.12 (medians of 5 runs):Old pattern: O(2^(n/4)) on non-matching dotted runs. New pattern: O(n).
Behavior equivalence
No functional change intended, and fuzz-verified:
re.sub()output (not just match/no-match), including group captures — zero divergences.tests/agent/test_redact.py.Test added
TestConfigKeyRedosResistance:test_long_dotted_run_completes_fast— 100-segment non-matching dotted run must complete in <1s (previously effectively hung).test_long_dotted_secret_still_redacted— deep dotted.password=key is still redacted, guarding against the rewrite changing semantics.