mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
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 <yannduzelier@gmail.com>
This commit is contained in:
1 parent
e64930dbec
commit
606bfcb771
2 files changed
+59
-4
No files matched your search
+32
-4
@@ -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("<secret>")`) 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", "<secret>")`). 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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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")',
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user