Skip to content

fix(sandbox): avoid shell in FsBridge writes#1155

Open
evgyur wants to merge 1 commit into
nextlevelbuilder:devfrom
evgyur:fix/fsbridge-write-shell-injection
Open

fix(sandbox): avoid shell in FsBridge writes#1155
evgyur wants to merge 1 commit into
nextlevelbuilder:devfrom
evgyur:fix/fsbridge-write-shell-injection

Conversation

@evgyur
Copy link
Copy Markdown

@evgyur evgyur commented May 19, 2026

Summary

  • Fixes FsBridge.WriteFile command injection by removing the sh -c write path.
  • Passes the sandbox target filename as a discrete argv entry to tee instead of interpolating it into a shell command string.
  • Adds regression tests for command-substitution-style filenames and append mode.

Security impact

A path such as notes/$(touch /tmp/pwned).txt is now treated as a literal filename argument instead of being evaluated by the shell.

Test plan

  • gofmt -w internal/sandbox/fsbridge.go internal/sandbox/fsbridge_test.go
  • go test ./internal/sandbox -count=1
  • git diff --cached --check

Closes #1121

Write sandbox files through tee with the target path passed as a discrete argv entry, so shell metacharacters in filenames are not evaluated by sh -c.

Adds regression coverage for command-injection-style filenames and append mode.
@chloeassistant
Copy link
Copy Markdown

🔒 Security Review Requested (P1)

This PR fixes a command injection vulnerability in FsBridge.WriteFile — removes the sh -c write path. Filenames like notes/$(touch /tmp/pwned).txt are now treated as literal paths.

Scope: 2 files, +122/-6 lines, includes regression tests.
Closes: #1121

Assigning to the Bridge Crew security pipeline:

  • Worf — security review (threat model validation, injection vector confirmation)
  • Fast-track merge once approved.

⚠️ SLA: this is a command injection fix — every hour of delay is a window of exposure.

@chloeassistant
Copy link
Copy Markdown

🛡️ Security Review — PR #1155: FsBridge Command Injection Fix

Reviewer: Worf, Chefe de Segurança — Bridge Crew
Date: 2026-05-19
PR: #1155
Author: @evgyur (external contributor)
Closes: #1121 (CWE-78 — OS Command Injection, CVSS 9.8 Critical)
Verdict:APPROVE (with minor recommendations)


1. Vulnerability Summary

Original Vulnerability (CWE-78)

FsBridge.WriteFile in internal/sandbox/fsbridge.go:68-72 constructed a shell command via:

b.dockerExec(ctx, []byte(content), "sh", "-c", fmt.Sprintf("cat %s %q", redir, resolved))

Go's %q format verb wraps strings in double-quotes and escapes internal quotes/slashes, but does NOT neutralize Bash metacharacters ($(), backticks, ;, |, &). When executed under sh -c, the shell evaluates command substitutions inside double-quoted strings.

Exploitation: A path like notes/$(touch /tmp/pwned).txt produced:

sh -c 'cat > "/workspace/notes/$(touch /tmp/pwned).txt"'

→ Bash executes $(touch /tmp/pwned) as command substitution before cat runs.

Impact: Arbitrary command execution as root inside the sandbox container. CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 9.8 (Critical).

Fix Applied

The sh -c write path is removed entirely. Content is now written via tee, with the filename passed as a discrete argv entry:

args := []string{"tee"}
if appendMode { args = append(args, "-a") }
args = append(args, "--", resolved)
_, stderr, exitCode, err := b.dockerExec(ctx, []byte(content), args...)

This eliminates the shell interpretation layer — tee receives the filename as a direct argument, never passing through shell evaluation.


2. Remaining Injection Vectors — NEGATIVE FINDINGS ✅

2.1 Shell metacharacters in filename ✅ SAFE

All shell metacharacters are preserved as literal filename characters:

  • $(command) → literal dollar-paren chars
  • backticks → literal backtick chars
  • ;, |, &, >, < → literal characters
  • The shell is never invoked, so none of these are interpreted.

2.2 Option injection via tee ✅ SAFE

The -- delimiter before resolved signals end of options to tee:

  • notes/-o/dev/null.txttee -- /workspace/notes/-o/dev/null.txt
  • --helptee -- /workspace/--help
  • -atee -- /workspace/-a ✅ (in non-append mode, -a would be an option without --)

2.3 Path traversal ✅ UNCHANGED

resolvePath() is unchanged by this PR. Existing behavior:

  • Absolute paths outside workdir → fallback to workdir
  • Relative traversal (../../etc/passwd) → resolved via filepath.Join which cleans as it goes

The container already has read-only filesystem + cap-drop ALL as defense-in-depth.

2.4 Content injection via stdin ✅ SAFE

Content is passed via cmd.Stdin = bytes.NewReader(stdin) in dockerExec. Since stdin goes directly to tee's input without shell interpretation, content containing shell metacharacters is safe.

2.5 dockerExec command construction ✅ SAFE

dockerExec builds: docker exec [-i] <container-id> <args...>. No shell is invoked between docker and the container commands. The exec.CommandContext API passes arguments as discrete argv entries.


3. New Risks Introduced — NEGLIGIBLE

3.1 tee stdout capture

Behavior: tee writes to BOTH the file and stdout. The old cat > only wrote to the file.

Risk: File content captured in dockerExec's stdout buffer (a bytes.Buffer).

Assessment: Negligible risk. The stdout buffer is captured in dockerExec and discarded by WriteFile (blank identifier _). The buffer is garbage-collected. No information leakage to logs, Docker daemon, or external observers.

Recommendation (minor): Document this behavior in a code comment for future maintainers.

3.2 TOCTOU between mkdir and write

Behavior: mkdir -p creates directories, then tee writes to the file.

Risk: Between these operations, an attacker could theoretically replace the directory with a symlink.

Assessment: Existing condition, not introduced by this PR. The sandbox container runs a single tenant agent process. Exploitation would require a concurrent malicious process inside the container, which is prevented by the sandbox isolation model. Mitigation (e.g., openat2 with O_NOFOLLOW) could be considered in a separate PR.

3.3 tee availability in minimal containers

Assessment: Same level of assumption as sh + cat. tee is part of GNU coreutils, available in all standard Docker images (Alpine, Debian, Ubuntu). If tee is missing, the error is caught (exitCode != 0) and propagated as "write failed".


4. Test Coverage Review

4.1 TestFsBridgeWriteFileDoesNotInvokeShell

Covers: Command injection via $(...) in filename

  • Uses malicious path nested/evil$(touch /tmp/goclaw-fsbridge-pwned);name.txt
  • Creates fake docker script to capture argv
  • Verifies: no sh -c, no cat >, uses tee -- <path>, filename preserved as one argv entry
  • Verifies stdin content integrity

Signal: Strong. Catches regression if someone re-adds shell invocation.

4.2 TestFsBridgeWriteFileAppendUsesTeeAppendArg

Covers: Append mode command structure

  • Verifies tee -a -- <path> for append mode

Signal: Moderate. Covers the append flag but doesn't test malicious paths in append mode.

4.3 Missing Coverage (Recommendations)

Test Case Severity Rationale
Option injection: filename -o/dev/null Low -- delimiter already protects this, but test would prevent regression
Append mode + malicious path Low Current append test only uses safe path append.txt
Filename with newlines Low Verify tee handles newlines in filenames correctly via argv
Filename with backticks Low Verify no shell evaluation via tee

5. Code Quality Observations

5.1 Comment quality ✅

// Write content via stdin without invoking a shell. Passing the resolved path
// as a discrete argv entry prevents shell metacharacters in filenames from
// being interpreted as commands inside the sandbox container.

Clear, concise, explains WHY the change was made. Good for future maintainers.

5.2 Consistency with ReadFile

ReadFile already used cat -- <path> without shell invocation. This fix brings WriteFile to parity.

5.3 Error handling ✅

Non-zero exit codes from tee are properly checked and returned as errors.


6. Verdict: ✅ APPROVE

The fix correctly and completely addresses the CWE-78 command injection vulnerability in FsBridge.WriteFile. The sh -c invocation is removed entirely, and filename arguments are passed as discrete argv entries to tee. The -- delimiter prevents option injection. Test coverage is adequate with strong regression protection.

Recommendations (non-blocking, for follow-up PR)

  1. Add option injection test: Test a filename like -o/dev/null to verify -- delimiter protection (tee -- /workspace/-o/dev/null).
  2. Add append + malicious path test: Test append mode with $(command) in filename to verify consistency with non-append mode.
  3. Comment on tee stdout: Note that tee writes content to stdout in addition to the file, and that the stdout buffer is intentionally discarded.

Threat Model Validation

Vector Before After Risk
$(cmd) in filename ❌ EXECUTED ✅ LITERAL Eliminated
Backticks in filename ❌ EXECUTED ✅ LITERAL Eliminated
;, |, & in filename ❌ PARSED ✅ LITERAL Eliminated
Option injection (--help) N/A via shell ✅ Blocked by -- Protected
Content via stdin ✅ Safe ✅ Safe Unchanged
Path traversal ✅ Container-scoped ✅ Container-scoped Unchanged

Qapla'. The fix is clean, minimal, and correct. Merge approved.


Reviewed by: 🛡️ Worf — Bridge Crew Security
Reference: CWE-78, CWE-88

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Arbitrary Command Execution inside Sandbox via FsBridge Command Injection

2 participants