From 57a1d957f4cee8f4e85d17e2b4944b41781033c1 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 16 Aug 2026 10:14:32 -0700 Subject: [PATCH] fix(redact): block real all-caps URL passwords, not just shape-match urlPasswordIsPlaceholder skipped any password matching /^[A-Z][A-Z0-9_]*$/, so a real DSN like postgres://admin:PROD2026SECRET@db-prod.internal/app slipped the HIGH pre-push block. Replace the shape rule with an anchored, exact-match set of doc-convention placeholder tokens (PASSWORD, PASS, CHANGEME, ...), compared case-sensitively and never as a substring (PROD2026SECRET must not match SECRET). The USER:PASSWORD doc convention still suppresses; real all-caps and lowercase passwords block. Regression cases pinned both directions. Co-Authored-By: Claude Fable 5 --- lib/redact-patterns.ts | 28 ++++++++++++++++++++++------ test/redact-engine.test.ts | 7 +++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 45a730c1a..cb4839758 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -268,17 +268,33 @@ export function insideUuid(match: RegExpExecArray): boolean { // alike (the identifier-only form flagged the DSN-encoding call site as a // pushed secret). Bare `$word` stays uppercase-only: `$hunter2` must block. const INTERPOLATED_PASSWORD_RE = /^(\$\{.+\}|\$[A-Z_][A-Z0-9_]*)$/; +// URL-password placeholders are matched by EXACT token, never by shape or +// substring. A shape rule (`/^[A-Z][A-Z0-9_]*$/`) waved through real all-caps +// secrets like `PROD2026SECRET`; a substring rule would let `PROD2026SECRET` +// slip because it contains `SECRET`. So this is an anchored, hand-curated set +// of the doc-comment conventions (postgres://USER:PASSWORD@host) only. Compared +// case-sensitively against the raw span: the convention is ALL CAPS, and a +// lowercase `password`/`pass` at this position is a real (terrible) credential +// that must still block. +const URL_PASSWORD_PLACEHOLDER_WORDS = new Set([ + "PASSWORD", + "PASS", + "PASSWD", + "YOUR_PASSWORD", + "DB_PASSWORD", + "MY_PASSWORD", + "CHANGEME", + "CHANGE_ME", + "PLACEHOLDER", + "REDACTED", + "EXAMPLE", +]); function urlPasswordIsPlaceholder(span: string): boolean { const m = span.match(/:\/\/[^:]+:([^@]+)@/); const pw = m?.[1] ?? ""; if (pw === "") return true; if (INTERPOLATED_PASSWORD_RE.test(pw)) return true; - // URL-password position is STRICTER than generic placeholder detection. - // Doc-comment convention writes placeholders in ALL CAPS - // (postgres://USER:PASSWORD@host); a lowercase `password` or `pass` at - // this position is a real (terrible) credential and must block — the - // case-insensitive isPlaceholderSpan words would wave it through. - if (/^[A-Z][A-Z0-9_]*$/.test(pw)) return true; + if (URL_PASSWORD_PLACEHOLDER_WORDS.has(pw)) return true; return PLACEHOLDER_STRUCTURAL.some((re) => re.test(pw)); } diff --git a/test/redact-engine.test.ts b/test/redact-engine.test.ts index 33771f904..e7e66bbb6 100644 --- a/test/redact-engine.test.ts +++ b/test/redact-engine.test.ts @@ -134,6 +134,13 @@ describe("HIGH credential patterns", () => { expect(ids("https://root:" + "pa" + "ss@127.0.0.1/")).toContain("creds.basic_auth_url"); // Structural placeholders still suppress at the URL position. expect(ids("postgres://user:@host/db")).not.toContain("db.url_with_password"); + // An ALL-CAPS password that is NOT an exact placeholder token is a real + // secret and must block — the pre-fix shape rule (/^[A-Z][A-Z0-9_]*$/) waved + // every all-caps password through. Substring of a placeholder word (SECRET) + // must not rescue it. Assembled at runtime so this file's own pushed bytes + // carry no live DSN shape. + expect(ids("postgres://admin:" + "PROD2026" + "SECRET@db-prod.internal/app")).toContain("db.url_with_password"); + expect(ids("postgres://admin:" + "ADMIN" + "123@host/db")).toContain("db.url_with_password"); }); test("all HIGH patterns block (exit 3)", () => {