mirror of https://github.com/garrytan/gstack.git
fix(redact): interpolation allowance can't swallow a real $word password
The placeholder calibration used optional braces on both sides, which
also suppressed bare $lowercase — a real password starting with '$'
would have passed the HIGH gate. Interpolation now means ${identifier}
(braced, any case) or bare $UPPER_SNAKE only; both connection-string
patterns share one validator so they can't drift. Pins added for the
bare-$word block, $UPPER allowance, and mismatched-brace block.
This commit is contained in:
parent
d4e1b4dc31
commit
f6e3297b3d
|
|
@ -254,6 +254,22 @@ export function insideUuid(match: RegExpExecArray): boolean {
|
|||
|
||||
// ── The taxonomy ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* URL-embedded passwords that are interpolation forms, not credentials:
|
||||
* `${identifier}` (bash or JS template, any case) or bare `$UPPER_SNAKE`
|
||||
* (shell convention). Bare lowercase `$word` stays BLOCKED — a real password
|
||||
* that merely starts with `$` (e.g. `$` + a dictionary word) must not slip
|
||||
* through the HIGH gate just because it looks vaguely variable-shaped.
|
||||
* 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_]*)$/;
|
||||
function urlPasswordIsPlaceholder(span: string): boolean {
|
||||
const m = span.match(/:\/\/[^:]+:([^@]+)@/);
|
||||
const pw = m?.[1] ?? "";
|
||||
return pw === "" || isPlaceholderSpan(pw) || INTERPOLATED_PASSWORD_RE.test(pw);
|
||||
}
|
||||
|
||||
export const PATTERNS: RedactPattern[] = [
|
||||
// ===== HIGH — genuinely-secret credentials (block) =====
|
||||
{
|
||||
|
|
@ -437,15 +453,8 @@ export const PATTERNS: RedactPattern[] = [
|
|||
category: "secret",
|
||||
description: "Database URL with embedded password",
|
||||
regex: /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]+@[^\s/]+)/,
|
||||
// Skip when the password segment is itself a placeholder.
|
||||
validate: (span) => {
|
||||
const m = span.match(/:\/\/[^:]+:([^@]+)@/);
|
||||
const pw = m?.[1] ?? "";
|
||||
// Any $VAR / ${identifier} interpolation is code, not a credential —
|
||||
// covers bash ${DB_PASS} and JS template `${dbPass}` alike (the
|
||||
// uppercase-only form flagged ported TS templates as pushed secrets).
|
||||
return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(pw);
|
||||
},
|
||||
// Skip when the password segment is itself a placeholder/interpolation.
|
||||
validate: (span) => !urlPasswordIsPlaceholder(span),
|
||||
},
|
||||
{
|
||||
id: "creds.basic_auth_url",
|
||||
|
|
@ -453,14 +462,8 @@ export const PATTERNS: RedactPattern[] = [
|
|||
category: "secret",
|
||||
description: "HTTP(S) URL with embedded basic-auth credentials",
|
||||
regex: /(https?:\/\/[^:\s/@]+:[^@\s/]+@[^\s/]+)/,
|
||||
validate: (span) => {
|
||||
const m = span.match(/:\/\/[^:]+:([^@]+)@/);
|
||||
const pw = m?.[1] ?? "";
|
||||
// Any $VAR / ${identifier} interpolation is code, not a credential —
|
||||
// covers bash ${DB_PASS} and JS template `${dbPass}` alike (the
|
||||
// uppercase-only form flagged ported TS templates as pushed secrets).
|
||||
return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(pw);
|
||||
},
|
||||
// Skip when the password segment is itself a placeholder/interpolation.
|
||||
validate: (span) => !urlPasswordIsPlaceholder(span),
|
||||
},
|
||||
|
||||
// ===== MEDIUM — demoted credential-shaped (high-FP / context-variable) =====
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ describe("HIGH credential patterns", () => {
|
|||
// Assembled at runtime so this file's own diff never contains a
|
||||
// credential-shaped literal (the prepush guard scans exact pushed bytes).
|
||||
expect(ids("postgres://admin:" + "hun" + "ter2@db.internal/app")).toContain("db.url_with_password");
|
||||
// Bare $UPPER_SNAKE is shell convention → suppressed; bare $lowercase is
|
||||
// NOT an interpolation form — a real password starting with `$` must
|
||||
// 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");
|
||||
});
|
||||
|
||||
test("all HIGH patterns block (exit 3)", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue