From 606bfcb771a842c1fa8bcc1eb6c7d5d3d10ca226 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 20:37:11 +0000 Subject: [PATCH] H #2899: env.kv skips a function call assigned to a credential name `session = _FlakySession(...)`, `token = make_token(user, ...)` and `password = getpass.getpass()` raised env.kv MEDIUM, which blocks the fail-closed decision log. The exemption needs the line's evidence, not the span's shape: the value is unquoted, is an identifier or dotted path followed by "(", and the call closes on the line or opens a multi-line argument list. Quoted values, unclosed "(" inside an unquoted secret (API_SECRET=Abc123xyz(9Qm) and calls carrying a high-entropy literal argument still report. The literal check is shared with the #2912 env-read exemption. Co-authored-by: Yugz --- lib/redact-patterns.ts | 36 +++++++++++++++++++++++--- test/redact-context-exemptions.test.ts | 27 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 127b4de0e..858ade4f2 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -482,10 +482,35 @@ const ENV_READ_SPAN = /^(?:os\.environ\[|os\.environ\.get\(|os\.getenv\(|getenv\(|ENV\[|process\.env\.[A-Za-z_$][\w$]*[;,)]?)$/; function isBareEnvRead(span: string, match: RegExpExecArray): boolean { if (!ENV_READ_SPAN.test(span)) return false; - const rest = match.input.slice(match.index + match[0].length).split("\n", 1)[0]; - for (const [, literal] of rest.matchAll(/["']([^\s'"]{8,})["']/g)) - if (!isPlaceholderSpan(literal) && shannonEntropy(literal) >= 3.0) return false; - return true; + return !carriesSecretLiteral(match.input.slice(match.index + match[0].length).split("\n", 1)[0]); +} + +/** True when `text` holds a quoted, non-placeholder, high-entropy literal. */ +function carriesSecretLiteral(text: string): boolean { + for (const [, literal] of text.matchAll(/["']([^\s'"]{8,})["']/g)) + if (!isPlaceholderSpan(literal) && shannonEntropy(literal) >= 3.0) return true; + return false; +} + +/** + * #2899: the value capture is any non-space run, so it swallows code: + * `session = _FlakySession(responses=[...])`, `token = make_token(user,`, + * `password = getpass.getpass()`. A bare credential name plus mixed-case code + * clears the entropy gate. The span alone cannot tell `Abc123(xyz` (a real + * password) from a call, so the exemption needs the line's evidence: the + * value is unquoted, starts with an identifier or dotted path followed by + * `(`, and that call closes on the line or opens a multi-line argument list. + * A quoted value is a literal, and a call whose arguments carry a + * high-entropy literal (`decrypt("")`) still reports. + */ +const CALL_SHAPED_VALUE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\(/; +function isCallExpression(span: string, match: RegExpExecArray): boolean { + if (!CALL_SHAPED_VALUE.test(span)) return false; + const { start } = spanBounds(match); + if (match.input[start - 1] === '"' || match.input[start - 1] === "'") return false; + const call = match.input.slice(start + span.indexOf("(")).split("\n", 1)[0]; + if (!call.includes(")") && call.trim() !== "(") return false; + return !carriesSecretLiteral(call); } export const PATTERNS: RedactPattern[] = [ @@ -760,11 +785,14 @@ export const PATTERNS: RedactPattern[] = [ // holding one, unless the rest of its line carries a high-entropy quoted // literal (`os.getenv("X", "")`). A literal appended to the read // itself (`process.env.X||"…"`) is not an exact read and still fires. + // #2899: a function call assigned to the name is code, not a value (see + // isCallExpression). validate: (span, match) => isCredentialShapedEnvName(match[0]) && !isPlaceholderSpan(span) && !/^\$\{?[A-Za-z_]/.test(span) && !isBareEnvRead(span, match) && + !isCallExpression(span, match) && shannonEntropy(span) >= 3.0, }, { diff --git a/test/redact-context-exemptions.test.ts b/test/redact-context-exemptions.test.ts index d0967b735..b4b3b588e 100644 --- a/test/redact-context-exemptions.test.ts +++ b/test/redact-context-exemptions.test.ts @@ -78,3 +78,30 @@ describe("pii.phone.e164 / pii.cc: vector geometry is not PII (#2885, #2827)", ( } }); }); + +describe("env.kv: a function call assigned to a credential name is code (#2899)", () => { + table( + "env.kv", + "MEDIUM", + [ + ' session = _FlakySession(requests.ReadTimeout("Read timed out"))', + " session = _FlakySession(responses=[1])", + " session = requests.Session()", + " session = find_session(client, args.session_id, now=now, state=state)", + ' token = make_token(user, scopes=["read"])', + " password = getpass.getpass()", + " session = build_http_session(", + ], + [ + 'session = "Xk9pQ2mLr7Tz4vBn"', + "SESSION_SECRET=9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c", + 'password = "Tr0ub4dor&3!xyz"', + "PASSWORD=Tr0ub4dor&3!xyz#", + "password = correct.horse.battery.staple", + 'api_key: "Zx9Qw8Er7Ty6Ui5Op4As3Df2"', + "API_SECRET=Abc123xyz(9Qm", + 'password = "getPassword(x)Q9z"', + 'token = decrypt("Zx9Qw8Er7Ty6Ui5Op4As3Df2")', + ], + ); +});