fix(#4639): exempt the container --env-file value from the secret-read guard (#4789)

* fix(#4639): exempt the container --env-file value from the secret-read guard

* test(#4639): adopt the suite assertions and pin the reclassified row

* docs(#4639): document and pin the container-env printenv residual

* docs(#4639): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-16 06:44:55 -04:00
committed by GitHub
parent 003d982c83
commit f0a1745e29
3 changed files with 106 additions and 2 deletions

View File

@@ -86,7 +86,12 @@
// on plain commands, without arming the compound-`cd` prompt". Writes to
// secret files are out of scope (Write/Edit were never gated). Commands
// over 1 MiB are denied outright (`command-too-large`) rather than
// scanned partially or waved through.
// scanned partially or waved through. (#4639 adds one more, by design: the
// value of `--env-file` under a container runtime is exempt, so the
// container's own command can print the interpolated environment
// (`alpine printenv`, `docker compose config`) — the same exposure class
// as the pre-existing volume-mount gap (`-v .env:/s`); the flag's value
// itself is a name, never contents.)
//
// Triggers on: Read, Grep, Bash tool calls (Kimi: ReadFile, Grep, Shell)
// Action: BLOCK (decision: 'block', exit 2) — codes secret-read |
@@ -127,6 +132,15 @@ const NON_READING_COMMANDS = new Set([
'basename', 'dirname', 'realpath', 'file', 'echo', 'printf',
]);
// #4639: container runtimes take `--env-file <file>` — the runtime opens the
// file itself, interpolates it into the container environment, and returns
// nothing to the agent, so the flag's VALUE is a name, never contents. The
// same category NON_READING_COMMANDS encodes, expressed as a flag value. Only
// the flag's value under these runtimes is exempt; every other operand in the
// segment is still checked, so the carve-out cannot launder a read.
const CONTAINER_RUNTIMES = new Set(['docker', 'docker-compose', 'podman', 'nerdctl']);
const ENV_FILE_FLAG_RE = /^--env-file(=|$)/;
// Shell interpreters that run a script from `-c`, a file operand, or stdin
// (heredoc / here-string / piped `echo`|`printf`). `su` is here for its `-c`
// form (`su [user] -c 'cmd'`); a bare `su user` resolves to file mode, which
@@ -840,7 +854,19 @@ function findSecretRead(command, depth) {
if (NON_READING_COMMANDS.has(base)) continue;
// #4639: exempt ONLY the value of `--env-file`, and only when the
// segment's command word is a container runtime. A bare `--env-file`
// consumes the next operand; `--env-file=<value>` is a single word.
const envFileExempt = CONTAINER_RUNTIMES.has(base);
let skipNext = false;
for (const w of operands) {
if (envFileExempt) {
if (skipNext) { skipNext = false; continue; }
if (ENV_FILE_FLAG_RE.test(w.text)) {
if (!w.text.includes('=')) skipNext = true; // bare flag consumes the next operand
continue;
}
}
if (namesSecret(normalizeOperand(w.text))) return w.text;
}
}