fix(redact): keep dotted config-key scans linear past the keyword pre-gate

The _CFG_SECRET_WORD_RE pre-gate only skips secret-FREE text. A compaction
payload containing one real secret assignment plus a long opaque dotted run
still reaches _CFG_DOTTED_RE's backtrackable '*' prefix, which re.sub retries
from every byte of the run — quadratic while holding the GIL (same class as
the _ENV_ASSIGN_LOWER_RE fix in this branch, #99255).

Anchor each attempt to the start of a key run with a negative lookbehind.
Match set is unchanged: any match starting mid-run implies a leftmost match
at the run start, verified 20/20 identical over a dotted-config corpus.
30k-char adversarial run: 102s -> 0.015s.
This commit is contained in:
kshitijk4poor
2026-08-31 23:22:30 +05:30
committed by kshitij
parent e60bd6aed2
commit fe0cfdf99c
2 changed files with 24 additions and 0 deletions

View File

@@ -204,7 +204,14 @@ _ENV_LOOKUP_VALUE_RE = re.compile(
# ``(?:[A-Za-z0-9_\-]+\.)+`` (exponential backtracking on long dotted runs).
# The ``*`` runs bordering {_SECRET_CFG_NAMES} must stay backtrackable
# (secret words are matchable by the class, e.g. ``app.api.key=…``).
# The lookbehind anchors each attempt to the start of a key run: without it,
# ``re.sub`` retries the backtrackable ``*`` prefix at every byte of a long
# non-matching dotted run, making the sub quadratic whenever the text contains
# a secret keyword anywhere (the ``_CFG_SECRET_WORD_RE`` pre-gate only skips
# secret-free text). Match set is unchanged — any match starting mid-run
# implies a leftmost match starting at the run start (#99255).
_CFG_DOTTED_RE = re.compile(
rf"(?<![A-Za-z0-9_.\-])"
rf"([A-Za-z0-9_\-]++\.[A-Za-z0-9_.\-]*{_SECRET_CFG_NAMES}[A-Za-z0-9_.\-]*+"
rf"|[A-Za-z0-9_.\-]*{_SECRET_CFG_NAMES}[A-Za-z0-9_.\-]*\.[A-Za-z0-9_.\-]++)"
rf"={_CFG_VALUE}",

View File

@@ -690,6 +690,23 @@ class TestConfigKeyRedosResistance:
assert redact_sensitive_text(text, force=True) == text
assert time.perf_counter() - t0 < 2.0
def test_dotted_cfg_scan_stays_linear_with_keyword_elsewhere(self):
"""_CFG_DOTTED_RE must stay linear once the pre-gate passes.
The ``_CFG_SECRET_WORD_RE`` pre-gate only skips secret-FREE text, so a
payload that contains a real secret assignment AND a long opaque
dotted run still reaches the backtrackable ``*`` prefix. Without the
run-start lookbehind the sub retries that prefix from every byte of
the run (quadratic while holding the GIL).
"""
import time
text = "password=hunter2\n" + "a." * 15_000 + "=value"
t0 = time.perf_counter()
result = redact_sensitive_text(text, force=True)
assert "hunter2" not in result
assert time.perf_counter() - t0 < 2.0
def test_yaml_assign_redos_resistance(self):
"""_YAML_ASSIGN_RE must not backtrack excessively on long inputs."""
import time