Conversation
…t vars
`Bash.exec(..., { env, replaceEnv: true })` put the per-exec variables in
the environment map but never marked them exported, and nested `sh`/`bash`
merged its exported env onto the persistent shell instead of replacing it.
A nested shell therefore missed the per-exec variables while still seeing
the constructor's env, and unexported variables leaked into children.
- Bash.exec marks per-exec env keys as exported (valid names only, so the
positional parameters nested shells pass through env stay local); with
replaceEnv it starts from an empty export set. The set is copied per exec
so an `export` in one exec no longer leaks into later ones.
- replaceEnv keeps the shell-maintained SHELLOPTS and BASHOPTS.
- Nested sh/bash runs with replaceEnv, starting from the exported env plus
what a new shell sets up itself: a default PATH, host variables
(OSTYPE, HOSTNAME, ...) and IFS/OPTIND, which bash never imports.
Fixes vercel-labs#438
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
🤖 auto-maintain reviewAutomated, advisory triage for
Review panel: 🔴 high highest severity just-bash maintainer code review: 🟡 medium
General code review: 🔴 high
Adversarial security: 🟡 medium
Adversarial security (second opinion): 🟡 medium
Standard Bash and host portability: 🟡 medium
Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call. |
…e leak Address review on vercel-labs#439: - Only the host `Bash.exec` API and new shells treat `env` as the environment and export its keys. Internal callers such as `env` and `time` pass the full variable map, so exporting it re-leaked unexported variables (`FOO=secret; env sh -c 'echo $FOO'`). - Nested sh/bash now asks for a new shell (`newShell`) instead of seeding variables through `env`. Bash.exec initializes PATH, OSTYPE, HOSTNAME, ... from fixed defaults when the environment lacks them, and always resets IFS, OPTIND, SHELLOPTS and BASHOPTS, all unexported. Unexported parent values (`HOSTNAME=secret; export -n HOSTNAME`) no longer reach the child, and `export -p` there no longer lists them. - SHELLOPTS/BASHOPTS are no longer injected into a host `replaceEnv` exec, so `env -i printenv` stays empty of them. - `replaceEnv` no longer falls back to the parent's previous directory, so `cd -` in a child without OLDPWD reports "OLDPWD not set". - Drop the banned `|| {}` fallback and add a changeset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks. All findings are addressed in 2780bf9:
Each finding has a regression test in |
|
A new push changed this PR and the review now contains a higher-severity finding. See the updated review comment above. |
Fixes #438.
Problem
Bash.exec(…, { env, replaceEnv: true })sets the per-exec variables for directly run commands, but a nestedsh/bashnever sees them, while the constructor's env (e.g.SECRET) still reaches it. There were two halves, as the issue describes:Bash.execadded per-exec keys toenvbut not toexportedVars. It also kept the parent's export set underreplaceEnv, and shared thatSetby reference, so anexportin oneexec()leaked into every later one.sh/bash(executeScript) called the nested exec withoutreplaceEnv, so the child merged onto the persistent shell's full env. That made unexported variables and parent secrets visible.Fix
Bash.ts): the export set is copied per exec. Only the hostBash.execAPI and new shells treatenvas the environment and export its keys, and only valid names count, so positional parameters (0,#,1) stay local. Internal callers such asenvandtimepass the full variable map, so they keep the persistent export set as before.newShellon the internalCommandExecOptions): nestedsh/bashpasses only the parent's exported env plus positional parameters, and asks for a new shell.Bash.execthen initializes what bash sets up at startup, all unexported:PATHand the host variables (OSTYPE,MACHTYPE,HOSTTYPE,HOSTNAME) get fixed defaults when the environment lacks themIFS,OPTIND,SHELLOPTSandBASHOPTSare always resetcd -: underreplaceEnvthe previous directory comes only from the providedOLDPWD, not the parent's state. A child that didn't inheritOLDPWDreportscd: OLDPWD not set.Top-level
replaceEnvstill starts from exactly the given env.printenvwith{ env: { A: "1" }, replaceEnv: true }prints onlyA=1.Behaviour (checked against
/bin/bash3.2.57 and bash 5.3)sh -c 'echo [$MARKER]; printenv SECRET'with{ env: {MARKER:"YES"}, replaceEnv: true }[]/leak[YES]/ exit 1export FOO=barin one exec, thenFOO=noleak; sh -c 'echo [$FOO]'[noleak][]FOO=secret; env sh -c 'echo [$FOO]'[][]export -n HOME; sh -c 'echo [$HOME]'[/home/user][]HOSTNAME=secret; export -n HOSTNAME; sh -c 'echo $HOSTNAME'secretunset PATH; sh -c 'IFS=:; export -p'PATH/IFS/OPTIND/…export IFS=: OPTIND=5; sh -c '…'IFS=:…IFS=' \t\n',OPTIND=1cd /tmp; export -n OLDPWD; sh -c 'cd -'/home/userOLDPWD not set, exit 1(
OSTYPE/HOSTNAMEdefaults are the sandbox'slinux-gnu/localhostrather than the host's values.)Out of scope, unchanged from
mainFOO=x env sh -c 'echo $FOO'prints[](bash:[x]), because theenvcommand doesn't mark its assignments exported.env -i printenvprintsPWD=….env/printenvlist all shell variables, not just exported ones.Testing
src/commands/bash/bash.env.test.ts(24 tests, one per scenario above). 6 of them cover the review findings and fail on the first revision of this PR.pnpm typecheck,pnpm lint:fix,pnpm lint:banned,pnpm knipare clean. There is a changeset.pnpm test:run: everything passes except 6 Python/WASM tests that time out loading CPython locally (they pass in CI).🤖 Generated with Claude Code