mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
410b4928e7
commit
57a1d957f4
+22
-6
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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:<your-password>@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)", () => {
|
||||
|
||||
Reference in New Issue
Block a user