Merge pull request #1846 for October 14 maintenance

This commit is contained in:
Haozhe Wu committed 2026-10-14 22:17:09 +08:00
commit 7399de116a
2 files changed
+40 -3

No files matched your search

+19 -3
View File
@@ -427,7 +427,8 @@ _TEXT_VALUE = r"\"[^\"\n]*\"|'[^'\n]*'|[^\s,;{}\[\]()\"']+"
#: backreference. The negative lookahead keeps the substitution idempotent (an
#: already-redacted value is skipped) and defers ``Bearer <jwt>`` to
#: :data:`_TEXT_BEARER_PATTERN`, which would otherwise be consumed as the value
#: and leave the token body visible.
#: and leave the token body visible, and skips a scheme word that
#: :data:`_TEXT_AUTH_SCHEME_PATTERN` already followed with the sentinel.
#:
#: Every quantifier applies to a bounded character class, with no nesting and
#: no overlapping alternatives, so matching stays linear (no ReDoS surface).
@@ -441,7 +442,9 @@ _TEXT_KV_PATTERN = re.compile(
r"(?:\b|(?<=(?-i:[A-Z0-9])_)|(?<=aws_))"
r"(?P<name>" + _TEXT_CREDENTIAL_KEYS + r")\b(?P=q)"
r"(?P<sep>\s*[:=]\s*)"
r"(?![\"']?" + re.escape(_REDACTED) + r"[\"']?|bearer\b)"
r"(?![\"']?" + re.escape(_REDACTED) + r"[\"']?"
r"|(?:bearer|basic|digest|token|negotiate|ntlm)\s+" + re.escape(_REDACTED) +
r"|bearer\b)"
r"(?P<value>" + _TEXT_VALUE + r")",
re.IGNORECASE,
)
@@ -454,6 +457,18 @@ _TEXT_BEARER_PATTERN = re.compile(
re.IGNORECASE,
)
#: ``Authorization: Basic <base64>`` and the other non-bearer schemes. The value
#: is ``user:password`` in base64, which the key pattern cannot see: it would
#: redact only the scheme word and leave the credential after it. Matched only
#: directly after an ``authorization`` label, so prose such as "basic
#: information" is untouched. Applied before :data:`_TEXT_KV_PATTERN`.
_TEXT_AUTH_SCHEME_PATTERN = re.compile(
r"(?P<head>\bauthorization[\"']?\s*[:=]\s*[\"']?)"
r"(?P<scheme>basic|digest|token|negotiate|ntlm)\s+"
r"[^\s\"',;{}]+",
re.IGNORECASE,
)
#: URL userinfo (``scheme://user:password@host``). It is a credential with no
#: key label, so the key-based pattern never reaches it. A password may contain
#: ``@``, so the match runs to the last ``@`` before a delimiter (whitespace,
@@ -511,7 +526,7 @@ def redact_text(text: object) -> str:
Used for plain-text tool results, shell output, error messages and log
lines where :func:`redact_payload` is a no-op (it only walks structured
keys). Bearer header values and ``key=value`` / ``key: value`` /
keys). Bearer and Basic header values and ``key=value`` / ``key: value`` /
``"key": "value"`` credential pairs are replaced with ``'[redacted]'``,
and URL userinfo (``scheme://user:password@host``) becomes
``scheme://[redacted]@host``, while the surrounding text — labels, paths,
@@ -532,6 +547,7 @@ def redact_text(text: object) -> str:
if not s:
return s
s = _TEXT_BEARER_PATTERN.sub(r"\g<scheme> " + _REDACTED, s)
s = _TEXT_AUTH_SCHEME_PATTERN.sub(r"\g<head>\g<scheme> " + _REDACTED, s)
s = _TEXT_URL_USERINFO_PATTERN.sub(_REDACTED + "@", s)
s = _TEXT_KV_PATTERN.sub(_sub_credential_pair, s)
# Bare tokens with no key label in front of them.
+21
View File
@@ -150,6 +150,16 @@ _CREDENTIAL_SHAPES = (
("authorization=raw-token-value", "authorization=[redacted]"),
("Authorization: Bearer eyJhbGciOi.J9-_x==", "Authorization: Bearer [redacted]"),
("bearer abc123", "bearer [redacted]"),
# Basic and other non-bearer schemes: the base64 credential follows the
# scheme word, so the pair is redacted together, not just the word.
("Authorization: Basic dXNlcjpwYXNz", "Authorization: Basic [redacted]"),
(
"Proxy-Authorization: Basic dXNlcjpwYXNz",
"Proxy-Authorization: Basic [redacted]",
),
("authorization=Basic dXNlcjpwYXNz", "authorization=Basic [redacted]"),
("Authorization: token ghp_example", "Authorization: token [redacted]"),
('{"Authorization": "Basic dXNlcjpwYXNz"}', '{"Authorization": "[redacted]"}'),
# Env-style names: the label is a suffix of a longer identifier, so the
# leading word boundary used to reject it (``_`` is a word character).
("TUSHARE_TOKEN=abc123", "TUSHARE_TOKEN=[redacted]"),
@@ -233,6 +243,9 @@ def test_redact_text_is_idempotent(raw: str) -> None:
'{"url":"https://api.x.com","email":"bob@x.com"}',
# A short SSH username is not a credential; keep it readable.
"git+ssh://git@github.com/org/repo.git",
# ``basic`` and ``authorization`` alone are prose, not a credential.
"Show basic information for the backtest",
"authorization server returned 200 OK",
],
)
def test_redact_text_leaves_benign_output_readable(raw: str) -> None:
@@ -251,3 +264,11 @@ def test_redact_text_none_and_empty_and_nonstr_safe() -> None:
assert redact_text(None) == ""
assert redact_text("") == ""
assert redact_text(42) == "42"
def test_redact_tool_result_scrubs_basic_authorization_in_stdout() -> None:
"""A Basic credential in a JSON result's free-text field must not survive."""
raw = '{"stdout": "curl -H \'Authorization: Basic dXNlcjpwYXNz\' https://x"}'
out = red_mod.redact_tool_result(raw)
assert "dXNlcjpwYXNz" not in out
assert "Authorization: Basic [redacted]" in out