google-gemini / google-gemini/gemini-cli
security(sandbox): host shell injection via BUILD_SANDBOX path interpolation in sandbox.ts
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
### What happened?
When `BUILD_SANDBOX=1` is set, `packages/cli/src/utils/sandbox.ts` builds a custom sandbox image. The code interpolates filesystem paths directly into a shell command string executed via `execSync` with `shell: true` (default):
`packages/cli/src/utils/sandbox.ts` (main @ 812f7a2bc) lines 338-366:
```ts
const projectSandboxDockerfile = path.join(GEMINI_DIR, 'sandbox.Dockerfile');
let buildArgs = '';
if (isCustomProjectSandbox) {
buildArgs += `-f ${path.resolve(projectSandboxDockerfile)} -i ${image}`;
}
execSync(`cd ${gcRoot} && node scripts/build_sandbox.js -s ${buildArgs}`, {
stdio: 'inherit',
env: { ...process.env, GEMINI_SANDBOX: command },
});
```
`gcRoot` is derived from `fs.realpathSync(process.argv[1]).split('/packages/')[0]` and `buildArgs` includes `path.resolve(projectSandboxDockerfile)`. Neither is shell-escaped. Both can contain spaces, `$`, `;`, backticks, or other shell metacharacters if the repository is checked out into a directory with those characters (e.g., attacker-controlled ZIP, shared filesystem, or `git clone` into `/tmp/evil; touch /tmp/pwned; echo`).
The same pattern appears at lines 551-564 for network creation:
```ts
execSync(
`${command} network inspect ${SANDBOX_NETWORK_NAME} || ${command} network create ${networkFlags} ${SANDBOX_NETWORK_NAME}`,
{ stdio: 'ignore' },
);
```
While `command` and `image` are validated, file paths are not. An attacker controlling the checkout path can execute arbitrary host commands outside the sandbox with the user's UID before the container even starts.
Note: the `image` variable is validated (`/^[a-zA-Z0-9_.:/-]+$/`), but `gcRoot` and `projectSandboxDockerfile` paths are not.
### What did you expect to happen?
Filesystem paths interpolated into shell strings must be shell-escaped (e.g., via `quote([path])` from `shell-quote`, already imported in this file) or the command should use `execFile`/`spawn` with `shell: false` and explicit argv arrays.
Suggested fix:
```ts
execSync(`cd ${quote([gcRoot])} && node scripts/build_sandbox.js -s ${buildArgs}`, ...)
```
or better, use `spawn` with `shell: false`:
```ts
execFileSync('node', ['scripts/build_sandbox.js', '-s', buildArgs], { cwd: gcRoot })
```
Similarly for network commands, use `spawn`/`execFile` with array args (as done correctly for `LXC proxyCommand` at lines 796-826).
### Client information
- Source-level finding verified against upstream `main` at commit `812f7a2bc`
- Affects all platforms when `BUILD_SANDBOX=1` is set (Linux/macOS container path; Windows not applicable for this code path but same pattern exists)
- File: `packages/cli/src/utils/sandbox.ts:338-366, 551-564`
### Login information
Not applicable.
### Anything else we need to know?
Sources:
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/cli/src/utils/sandbox.ts#L338-L366
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/cli/src/utils/sandbox.ts#L551-L564
- Related: LXC proxyCommand handling at lines 796-826 correctly uses `spawn(..., {shell: false})` — demonstrating the intended safe pattern
Searched existing issues for "BUILD_SANDBOX", "sandbox shell injection", "sandbox injection" — no open duplicate found (only closed build-failure issues #14506, #3404, etc.).
Contributor guide
Research direction
Start in packages/cli/src/utils/sandbox.ts at lines 338-366 and 551-564, tracing the BUILD_SANDBOX build and network command entry points. Compare them with the safe LXC proxyCommand handling at lines 796-826 and verify behavior with repository paths containing spaces and shell metacharacters. Done means checkout-derived paths cannot be interpreted as shell syntax before the sandbox starts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100