-
Notifications
You must be signed in to change notification settings - Fork 0
fix(security): isolate web E2E commands and readiness probes #1347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
3db0789
fix(security): require OS isolation for web e2e commands
seonghobae d42a97f
fix(security): harden loopback and document isolation
seonghobae 823cd89
fix(security): mount isolated root filesystem
seonghobae ce93f55
docs(security): document isolated web verification
seonghobae 871d50b
fix(e2e): cover and harden isolated command paths
seonghobae a5bdec8
Merge branch 'main' into fix/sandboxed-web-e2e-isolation-clean
seonghobae 8b9d884
docs: make sandbox changelog actionable
seonghobae c50e26b
fix(e2e): fail closed on rejected sandbox commands
seonghobae 391233f
fix(e2e): validate readiness before launch
seonghobae 524093e
fix(e2e): back off after server readiness errors
seonghobae 9951601
Merge branch 'main' into fix/sandboxed-web-e2e-isolation-clean
opencode-agent[bot] 7ac8298
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
claude 1134266
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
claude 6ed4466
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude c01c1aa
fix(e2e): validate readiness ports and fail closed on isolation gaps
claude 4088430
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude 96f82b7
fix(sandboxed-verify): use strict resolve() for symlink escape check
claude bde444d
fix(e2e): mirror full probe operations, exclude credential paths from…
claude cb25974
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude 583af50
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
claude be77d29
fix(sandboxed-verify): use lexical symlink walk, not resolve(), to re…
claude 2db32bb
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude fe237c4
fix(sandboxed-verify): fix off-by-one in symlink hop-limit walk
claude 46fdc2d
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
claude 297bcea
fix(sandboxed-verify): resolve symlink targets component-by-component
claude fe68c2f
fix(sandboxed-ci): repo launchers, env templates, shell shadowing, bl…
claude 89d0a45
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude bd0697a
fix(sandboxed-verify): track cycles with a recursive active-set, not …
claude 528a1ea
fix(sandboxed): report a clean coded failure when copy_workspace rejects
claude 5b96f84
fix(sandboxed-ci): honor explicit --ignore over env-template allowlis…
claude 3c32d3c
Merge remote-tracking branch 'origin/fix/sandboxed-web-e2e-isolation-…
claude ed05b55
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
49d4d6b
fix(sandboxed-web-e2e): close proxy bypass, port-occupancy, malformed…
claude d0c869c
Merge remote-tracking branch 'origin/main' into fix/sandboxed-web-e2e…
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Sandboxed web command isolation | ||
|
|
||
| `sandboxed_web_e2e.py` requires Linux `bubblewrap` (`bwrap`) by default. Each | ||
| backend, frontend, and E2E command runs with a fresh writable `tmpfs` root and | ||
| `/tmp`, plus one writable copied-repository bind at `/workspace`; the copied | ||
| repository and temporary homes are mapped there. Host runtime roots and the | ||
| minimal `/etc` identity, DNS, and time files are mounted read-only, so the host | ||
| filesystem is not reachable through absolute paths or `..` traversal. | ||
|
|
||
| Before wrapping a command, the helper resolves its executable and rejects paths | ||
| outside the read-only system roots mounted by bubblewrap. A tool installed in a | ||
| host-only location must be installed into one of those roots or the run exits | ||
| with code `126` before any service starts; the result marker records that code | ||
| and the selected backend. An executable that cannot be resolved on `PATH` at | ||
| all is rejected the same way — it is never handed unvalidated to bubblewrap or | ||
| the shell to resolve on its own. | ||
|
|
||
| A `bwrap` binary discovered on `PATH` is not by itself proof that isolation | ||
| works: a restricted host (unprivileged user namespaces disabled, or a | ||
| seccomp-restricted CI runner) can have the binary present yet unable to create | ||
| the requested namespaces. Before starting either service, `isolation_backend` | ||
| runs a bounded, cheap capability preflight that mirrors *every* operation | ||
| `isolated_command` actually performs — new-session creation, the new PID | ||
| namespace, tmpfs root, the standard read-only binds, `/proc`, `/dev`, a tmpfs | ||
| `/tmp`, and a writable bind+chdir into the same mount point real commands run | ||
| from, exercised against a real (throwaway) temp directory rather than a | ||
| trivial no-op. A reduced probe that skips one of these can pass on a host that | ||
| specifically denies that operation, then fail later once a real service | ||
| starts; mirroring the full set closes that gap. A non-zero exit, or a failure | ||
| to even launch the probe, is classified as isolation-unavailable and exits | ||
| with code `126`, the same as a missing `bwrap` binary, instead of surfacing | ||
| later as a confusing readiness or test failure. | ||
|
|
||
| Use `--isolation disabled` only for trusted local debugging. The result marker | ||
| records the requested mode and resolved backend so CI evidence cannot be | ||
| mistaken for an OS-isolated run. If required isolation is unavailable, the | ||
| command exits with code `126` before starting any service. | ||
|
|
||
| The workspace copy this helper and `sandboxed_verify.py` share | ||
| (`sandboxed_verify.copy_workspace`) preserves symlinks rather than | ||
| dereferencing them. Under `--isolation required`, a symlink whose absolute | ||
| target is not one of the explicitly bound paths already dangles safely | ||
| (`ENOENT`) inside bubblewrap's `tmpfs` root — verified empirically against | ||
| this code path. That containment does not extend to two paths that share the | ||
| same copy step: `--isolation disabled` (documented as trusted local debugging | ||
| only, but the copy itself makes no such distinction) runs the wrapped commands | ||
| directly on the host with no OS sandboxing at all, and `sandboxed_verify.py`'s | ||
| own verification command never runs inside bubblewrap in the first place. In | ||
| both, a repository-supplied symlink whose target is an absolute host path, or | ||
| a relative path with enough `..` segments to exit the copy, remains a live | ||
| symlink that a command following it can use to read or write host files | ||
| outside the intended workspace. Every symlink under the copy is therefore | ||
| resolved and checked against the workspace root immediately after | ||
| `shutil.copytree`, in `copy_workspace` itself so both callers get the same | ||
| protection; the first one found to escape fails the whole copy closed rather | ||
| than being silently dropped or repaired. | ||
|
|
||
| The writable `/workspace` mount is a copy of the caller's repository checkout, | ||
| not the checkout itself. `copy_workspace` (`scripts/ci/sandboxed_verify.py`) | ||
| excludes VCS/cache/build noise by default, and now also excludes common | ||
| credential-bearing dotfiles/dirs a checkout can carry (`.env*`, `.netrc`, | ||
| `.npmrc`, `.pypirc`, `.pgpass`, `.git-credentials`, `.ssh`, `.gnupg`, `.aws`, | ||
| `.kube`, `.docker`) so a repository that happens to have one of these present | ||
| at copy time never rides along into the sandboxed command's writable, | ||
| readable mount. The broad `.env*` exclusion has one deliberate carve-out: | ||
| `DEFAULT_ENV_TEMPLATE_ALLOWLIST` (`.env.example`, `.env.sample`, | ||
| `.env.template`) still copies those committed, secret-free dotenv templates | ||
| through, since verification commands read them for local defaults; a caller | ||
| can still force one of those names back out with an explicit `--ignore`. | ||
| Logs and the scrubbed per-command home directories are | ||
| intentionally part of that same writable mount — the tested command needs to | ||
| write them — this exclusion list narrows what "writable and readable by the | ||
| command under test" actually contains; it does not attempt to split the mount | ||
| by service. | ||
|
|
||
| Readiness polling remains loopback-only and does not follow redirects. Invalid | ||
| readiness URLs are reported as a coded readiness failure (`125`) rather than an | ||
| uncaught traceback. The network declaration is evidence metadata; callers that | ||
| need stronger network policy must run this helper inside a network-restricted | ||
| runner or container. | ||
|
|
||
| ## References | ||
|
|
||
| MITRE. (2026). *CWE-918: Server-side request forgery (SSRF)*. | ||
| https://cwe.mitre.org/data/definitions/918.html | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.