From a4ac56629a9782d3717e59fff956956974e0c6ac Mon Sep 17 00:00:00 2001 From: carlotestor Date: Mon, 20 Jul 2026 16:44:05 +0200 Subject: [PATCH 1/3] perf(redact): eliminate exponential backtracking in config-key patterns _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. --- agent/redact.py | 12 +++++++++--- tests/agent/test_redact.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/agent/redact.py b/agent/redact.py index b8b9be8912f9..ea70246a9079 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -171,9 +171,13 @@ r"^(?:os\.(?:getenv|environ)|process\.env|\$ENV\{)" ) # Namespaced (dotted) key: the secret word may sit anywhere in a dotted path. +# NOTE(perf): possessive quantifiers (py3.11+) replace the nested quantifier +# ``(?:[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=…``). _CFG_DOTTED_RE = re.compile( - 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"([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}", re.IGNORECASE, ) @@ -192,8 +196,10 @@ # is masked by _AUTH_HEADER_RE); ``auth_token``/``auth-token`` still match via # the ``token`` keyword. Quoted values defer to _JSON_FIELD_RE via the lookahead. _YAML_CFG_NAMES = r"(?:api[ _.\-]?key|token|secret|passwd|password|credential)" +# NOTE(perf): possessive quantifiers wherever the successor is disjoint; the +# leading ``[A-Za-z0-9_.\-]*`` stays backtrackable (see _CFG_DOTTED_RE note). _YAML_ASSIGN_RE = re.compile( - rf"(^[ \t]*[A-Za-z0-9_.\-]*{_YAML_CFG_NAMES}[A-Za-z0-9_.\-]*)(:[ \t]*)(?!['\"])([^\s&]+)", + rf"(^[ \t]*+[A-Za-z0-9_.\-]*{_YAML_CFG_NAMES}[A-Za-z0-9_.\-]*+)(:[ \t]*+)(?!['\"])([^\s&]++)", re.IGNORECASE | re.MULTILINE, ) diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index 2ea405f051a2..3d40be4e3f4f 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -545,6 +545,31 @@ def test_prose_mid_sentence_password_unchanged(self): +class TestConfigKeyRedosResistance: + """The dotted-key patterns must not backtrack exponentially (ReDoS). + + Before the possessive-quantifier rewrite, a non-matching run of ~40 + dotted segments took ~30ms and doubled every ~4 segments; 100 segments + would effectively hang the redactor (it runs on every log line). + """ + + def test_long_dotted_run_completes_fast(self): + import time + + # 100 dotted segments with no '=' — worst case for the old pattern. + text = ".".join(["segment"] * 100) + " end" + t0 = time.perf_counter() + assert redact_sensitive_text(text) == text + assert time.perf_counter() - t0 < 1.0 + + def test_long_dotted_secret_still_redacted(self): + # Possessive quantifiers must not change matching behavior. + text = ".".join(["seg"] * 50) + ".password=Sup3rS3cret!" + result = redact_sensitive_text(text) + assert "Sup3rS3cret!" not in result + assert ".password=" in result + + class TestXaiToken: KEY = "xai-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstu" From 5e3f22dd7219dc75583a93b7bf6556c060040f82 Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:43:34 +0530 Subject: [PATCH 2/3] test(redact): add YAML ReDoS test, strengthen existing test with keyword MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Relax timing bound from 1.0s to 2.0s (CI machine robustness). - Add test_long_dotted_run_with_keyword_completes_fast: includes a secret keyword so the pre-gate does NOT skip _CFG_DOTTED_RE — directly exercises the possessive-quantifier regex, not just the pre-gate. - Add test_yaml_assign_redos_resistance: _YAML_ASSIGN_RE was modified but had no ReDoS test — add 100-line stress input. - Add test_yaml_assign_secret_still_redacted: verify YAML matching behavior preserved with possessive quantifiers. --- tests/agent/test_redact.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index 3d40be4e3f4f..68307c7381d2 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -560,7 +560,22 @@ def test_long_dotted_run_completes_fast(self): text = ".".join(["segment"] * 100) + " end" t0 = time.perf_counter() assert redact_sensitive_text(text) == text - assert time.perf_counter() - t0 < 1.0 + assert time.perf_counter() - t0 < 2.0 + + def test_long_dotted_run_with_keyword_completes_fast(self): + """Exercise _CFG_DOTTED_RE directly (bypasses the keyword pre-gate). + + The pre-gate skips the regex when no secret keyword is present, so + test_long_dotted_run_completes_fast only guards the pre-gate. This + test includes a keyword but no '=' so the regex runs and must still + complete quickly thanks to the possessive quantifiers. + """ + import time + + text = ".".join(["segment"] * 100) + ".token end" + t0 = time.perf_counter() + assert redact_sensitive_text(text) == text + assert time.perf_counter() - t0 < 2.0 def test_long_dotted_secret_still_redacted(self): # Possessive quantifiers must not change matching behavior. @@ -569,6 +584,25 @@ def test_long_dotted_secret_still_redacted(self): assert "Sup3rS3cret!" not in result assert ".password=" in result + def test_yaml_assign_redos_resistance(self): + """_YAML_ASSIGN_RE must not backtrack excessively on long inputs.""" + import time + + # 100 lines of a long dotted key with a secret keyword but no + # matching colon-value form — stresses the regex without matching. + line = "a." * 50 + "token not_an_assignment" + text = "\n".join([line] * 100) + t0 = time.perf_counter() + redact_sensitive_text(text) + assert time.perf_counter() - t0 < 2.0 + + def test_yaml_assign_secret_still_redacted(self): + # Possessive quantifiers must not change YAML matching behavior. + text = "spring.datasource.password: hunter2" + result = redact_sensitive_text(text) + assert "hunter2" not in result + assert "password:" in result + class TestXaiToken: KEY = "xai-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstu" From 3e250c2ee9f623232113339e791f7a3d497ec572 Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:52:35 +0530 Subject: [PATCH 3/3] chore: add carlotestor to contributor email directory Required for check-attribution CI on salvage PR #76083. --- contributors/emails/carlotestor@users.noreply.github.com | 1 + 1 file changed, 1 insertion(+) create mode 100644 contributors/emails/carlotestor@users.noreply.github.com diff --git a/contributors/emails/carlotestor@users.noreply.github.com b/contributors/emails/carlotestor@users.noreply.github.com new file mode 100644 index 000000000000..0c29f6411780 --- /dev/null +++ b/contributors/emails/carlotestor@users.noreply.github.com @@ -0,0 +1 @@ +carlotestor