fix(redaction): match connection-string schemes case-insensitively

RFC 3986 schemes are case-insensitive, so POSTGRESQL+PSYCOPG2:// or
HTTPS://user:pass@host in tool output leaked the password past the
case-sensitive scheme alternation. Compile with IGNORECASE on both
sides of the FE/backend mirror; the structural userinfo requirement
is unchanged. Uppercase-scheme cases added to both test suites.
This commit is contained in:
Patrick Buckley
2026-07-06 21:42:16 -07:00
parent ed2623ff44
commit 19b1a04f17
4 changed files with 22 additions and 2 deletions
+4
View File
@@ -1350,6 +1350,10 @@ def test_redact_credentials_runtime_smoke() -> None:
+ "const apg = redactCredentials('postgresql+asyncpg://user:s3cret@db/app');\n"
+ "if (apg !== 'postgresql+asyncpg://user:[REDACTED:password]@db/app') "
+ "throw new Error('asyncpg conn redact failed: ' + apg);\n"
+ "// RFC 3986 schemes are case-insensitive - uppercase must not bypass\n"
+ "const up = redactCredentials('POSTGRESQL+PSYCOPG2://user:s3cret@db/app');\n"
+ "if (up !== 'POSTGRESQL+PSYCOPG2://user:[REDACTED:password]@db/app') "
+ "throw new Error('uppercase scheme conn redact failed: ' + up);\n"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".mjs", delete=False) as f:
f.write(harness)
+12
View File
@@ -250,6 +250,18 @@ class TestCredentialLeakage:
assert "s3cret_pass" not in r.sanitized, url
assert ":[REDACTED:password]@" in r.sanitized, url
def test_uppercase_scheme_connection_string(self) -> None:
# RFC 3986 schemes are case-insensitive; an uppercase scheme must
# not bypass redaction.
for url in (
"POSTGRESQL+PSYCOPG2://admin:s3cret_pass@db.internal/prod",
"HTTPS://admin:s3cret_pass@api.internal/x",
):
r = evaluate_output(url)
assert "connection_string_leak" in r.flags, url
assert r.sanitized is not None, url
assert "s3cret_pass" not in r.sanitized, url
def test_bearer_scheme_case_insensitive(self) -> None:
# RFC 7235 scheme name is case-insensitive.
r = evaluate_output("authorization: bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig12345")
+3
View File
@@ -98,9 +98,12 @@ _RE_PRIVATE_KEY_BLOCK = re.compile(
# covers SQLAlchemy dialect+driver URLs (``postgresql+psycopg2``,
# ``postgresql+asyncpg``, ``mysql+pymysql``) and ``mongodb+srv`` —
# enumerating drivers is a losing game, the suffix shape isn't.
# Schemes are case-insensitive per RFC 3986, hence IGNORECASE:
# ``POSTGRESQL://`` leaks the same password ``postgresql://`` does.
_RE_CONNECTION_STRING = re.compile(
r"(?:postgresql|mysql|mongodb|rediss?|amqps?|sqlite|https?)(?:\+[a-z0-9]*)?"
r"://[^:@\s]+:[^@\s]+@",
re.IGNORECASE,
)
_RE_ENV_SECRET_LINE = re.compile(r"[A-Z][A-Z_0-9]+=\S+")
_RE_ENV_SECRET_KEY = re.compile(
@@ -37,10 +37,11 @@ const _RE_PRIVATE_KEY_BLOCK =
// The optional +suffix covers SQLAlchemy dialect+driver URLs
// (postgresql+psycopg2, postgresql+asyncpg, mysql+pymysql) and
// mongodb+srv — enumerating drivers is a losing game, the suffix
// shape isn't.
// shape isn't. Schemes are case-insensitive per RFC 3986 (/i):
// POSTGRESQL:// leaks the same password postgresql:// does.
// ---------------------------------------------------------------------------
const _RE_CONNECTION_STRING =
/(?:postgresql|mysql|mongodb|rediss?|amqps?|sqlite|https?)(?:\+[a-z0-9]*)?:\/\/[^:@\s]+:[^@\s]+@/g;
/(?:postgresql|mysql|mongodb|rediss?|amqps?|sqlite|https?)(?:\+[a-z0-9]*)?:\/\/[^:@\s]+:[^@\s]+@/gi;
const _RE_CONN_USERINFO = /:\/\/([^:@\s]+):([^@\s]+)@/;