Merge pull request #1846 for October 14 maintenance
This commit is contained in:
commit
7399de116a
2 files changed
+40
-3
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user