diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 96979db07..45a730c1a 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -263,7 +263,11 @@ export function insideUuid(match: RegExpExecArray): boolean { * Shared by db.url_with_password and creds.basic_auth_url so the two * validators cannot drift. */ -const INTERPOLATED_PASSWORD_RE = /^(\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Z_][A-Z0-9_]*)$/; +// Fully-braced `${...}` spanning the whole password segment is template code +// regardless of content — `${dbPass}` and `${encodeURIComponent(dbPass)}` +// 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_]*)$/; function urlPasswordIsPlaceholder(span: string): boolean { const m = span.match(/:\/\/[^:]+:([^@]+)@/); const pw = m?.[1] ?? ""; diff --git a/test/redact-engine.test.ts b/test/redact-engine.test.ts index dd6da49a1..33771f904 100644 --- a/test/redact-engine.test.ts +++ b/test/redact-engine.test.ts @@ -119,8 +119,13 @@ describe("HIGH credential patterns", () => { // still block (both-braces-optional would have let it through). expect(ids("postgres://user:$DB_PASSWORD@host/app")).not.toContain("db.url_with_password"); expect(ids("postgres://admin:$" + "hun" + "ter2@db.internal/app")).toContain("db.url_with_password"); - // Mismatched brace is not an interpolation either. - expect(ids("postgres://admin:${dbPass@db.internal/app")).toContain("db.url_with_password"); + // Mismatched brace is not an interpolation either (assembled at runtime + // so this file's own pushed bytes carry no blockable URL shape). + expect(ids("postgres://admin:${" + "dbPass@db.internal/app")).toContain("db.url_with_password"); + // A fully-braced interpolation is code whatever it contains — the DSN + // builder's `${encodeURIComponent(dbPass)}` call site must not scan as a + // pushed secret. + expect(ids("postgresql://user:${encodeURIComponent(dbPass)}@host:5432/db")).not.toContain("db.url_with_password"); // A LOWERCASE literal 'password'/'pass' at the URL-password position is a // real (terrible) credential, not a doc placeholder — only the ALL-CAPS // doc convention (USER:PASSWORD) is suppressed. Assembled at runtime so