fix: only a lookup call's first argument is a name (GST-1064: make_password, hashpw, grouping parens and later statements still report)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-02 22:27:37 +00:00
1 parent eebdf83cfa
commit 0a10fc4d34
3 files changed
+25 -8

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
### Fixed
- **The secrets check no longer reads a file name or dict key inside a call as a credential (GST-1064).** `key = json.loads(files.get("key.json", ""))` failed pregate with `PREGATE_SECRETS` (`env.kv`): eight distinct letters in `"key.json"` reach the entropy bar, so the call's argument looked like a secret. So did `token = resp.json().get("access_token")`. A call's first argument that is a lowercase file name or a snake_case key of two or three words now counts as a name. A later argument or fallback is still a value: `os.getenv("SECRET_KEY", "orange_monkey_pass")` and `.get("access_token", "<key>")` still report. So do a real key in quotes or in a `.env` line, and a passphrase passed to a call (`decrypt("prod-signing-key")`). The entropy bar is unchanged.
- **The secrets check no longer reads a file name or dict key inside a call as a credential (GST-1064).** `key = json.loads(files.get("key.json", ""))` failed pregate with `PREGATE_SECRETS` (`env.kv`): eight distinct letters in `"key.json"` reach the entropy bar, so the call's argument looked like a secret. So did `token = resp.json().get("access_token")`. The first argument of a lookup call (`get`, `getenv`, `read`, `load`, `loads`, `open`, `pop`, `fetch`) that is a lowercase file name or a snake_case key of two or three words now counts as a name. Any other call still reports a password-shaped literal (`make_password("orange_monkey_pass")`, `bcrypt.hashpw(...)`), and so do a later argument or fallback (`os.getenv("SECRET_KEY", "orange_monkey_pass")`), a real key in quotes or in a `.env` line, and a kebab passphrase (`decrypt("prod-signing-key")`). The entropy bar is unchanged.
+9 -7
View File
@@ -662,24 +662,26 @@ function isSchemaFieldIdentifier(span: string, match: RegExpExecArray): boolean
return SCHEMA_FIELD_NAME.test(match[0]) && SCHEMA_FIELD_VALUE.test(span);
}
/**
* GST-1064: a call's first argument that is a lowercase file name
* GST-1064: the first argument of a lookup call (`get`, `getenv`, `read`,
* `load`, `loads`, `open`, `pop`, `fetch`) that is a lowercase file name
* (`files.get("key.json")`) or a snake_case key of two or three words
* (`resp.json().get("access_token")`) names what the call reads. Eight distinct
* letters already reach 3.0 bits, so entropy alone cannot tell it from a
* secret. Only that first position is a lookup: a later argument or fallback
* (`os.getenv("X", "orange_monkey_pass")`, `.get("k") or "…"`) is a value and
* is still checked, and so is a first argument with a digit, an uppercase
* letter, kebab words or four or more snake_case words.
* secret. Any other callee (`make_password("orange_monkey_pass")`), a grouping
* paren, a later argument or a fallback (`os.getenv("X", "orange_monkey_pass")`)
* is a value and is still checked, and so is a first argument with a digit, an
* uppercase letter, kebab words or four or more snake_case words.
*/
const LOOKUP_ARGUMENT =
/\([ \t]*(["'])(?:[a-z]+(?:_[a-z]+){1,2}|[a-z]+(?:[_-][a-z]+)*\.(?:json|ya?ml|toml|ini|cfg|conf|env|pem|crt|cer|key|txt|csv|xml|cookie))\1/g;
/(?<![\w$])(get|getenv|read|load|loads|open|pop|fetch)\([ \t]*(["'])(?:[a-z]+(?:_[a-z]+){1,2}|[a-z]+(?:[_-][a-z]+)*\.(?:json|ya?ml|toml|ini|cfg|conf|env|pem|crt|cer|key|txt|csv|xml|cookie))\2/g;
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.replace(LOOKUP_ARGUMENT, "("));
const line = match.input.slice(start).split("\n", 1)[0];
return !carriesSecretLiteral(line.replace(LOOKUP_ARGUMENT, "$1("));
}
/**
+15
View File
@@ -134,6 +134,21 @@ describe("env.kv: a name-shaped literal inside a call is a file or dict key, not
String.raw`token = resp.json().get("access_token", "orange_monkey_pass")`,
String.raw`key = derive_secret("tigers.horse")`,
String.raw`password = os.getenv("DB_PASSWORD", "qwhzxkvbnmplor.txt")`,
String.raw`password = make_password("orange_monkey_pass")`,
String.raw`password = bcrypt.hashpw("orange_monkey_pass", salt)`,
String.raw`token = jwt.encode("purple_walrus_pie", payload)`,
String.raw`api_key = settings.Secret("correct_horse_battery")`,
String.raw`password = hash_password("purple_walrus")`,
String.raw`password=hash_password("purple_walrus")`,
String.raw`+password = hash_password( "purple_walrus" )`,
String.raw`password = hash_password("purple_walrus").encode()`,
String.raw`password = hash_password("purple_walrus") # seed user`,
String.raw`password = config.get("db_password") or ("orange_monkey_pass")`,
String.raw`password = config.get("db_password", ("orange_monkey_pass"))`,
String.raw`password = login(user, ("purple_walrus"))`,
String.raw`password = load_cfg("app.yaml"); login("purple_walrus")`,
String.raw`password = login(user, str("purple_walrus"))`,
String.raw`password = encrypt(str("orange_monkey_pass"))`,
],
);
});