Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions agent/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
carlotestor
59 changes: 59 additions & 0 deletions tests/agent/test_redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,65 @@ 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 < 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.
text = ".".join(["seg"] * 50) + ".password=Sup3rS3cret!"
result = redact_sensitive_text(text)
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"

Expand Down
Loading