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:
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user