Skip to content

Commit a693467

Browse files
docs+ci: quick wins from external analysis
- THREAT_MODEL.md: model the opt-in js-exec/QuickJS surface (new 4.11), qualify the child_process claim in 3.5 (virtual shim), and state the no-bash-to-host-JS architectural invariant guarding 4.2/4.3 - CLAUDE.md: align WASM policy with reality (sql.js, quickjs-emscripten, vendored CPython as approved exceptions) - README: document node as a js-exec alias; list tar/yq/xan (and js-exec/node) as unavailable in browser builds - CI: add coverage.yml (non-gating coverage report artifact) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent bb9a20b commit a693467

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Coverage
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
coverage:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
lfs: true
17+
18+
- uses: pnpm/action-setup@v4
19+
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: "20"
23+
cache: "pnpm"
24+
25+
- name: Install dependencies
26+
run: pnpm install --frozen-lockfile
27+
28+
- name: Build
29+
run: pnpm build
30+
31+
- name: Run coverage
32+
run: pnpm --filter just-bash test:coverage
33+
34+
- name: Upload coverage report
35+
if: always()
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: coverage-report
39+
path: packages/just-bash/coverage/

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
just-bash is a TypeScript implementation of a bash interpreter with an in-memory virtual filesystem. Designed for AI agents needing a secure, sandboxed bash environment. No WASM dependencies allowed.
7+
just-bash is a TypeScript implementation of a bash interpreter with an in-memory virtual filesystem. Designed for AI agents needing a secure, sandboxed bash environment. No new WASM dependencies without approval; approved exceptions are sql.js (SQLite), quickjs-emscripten (js-exec), and the vendored CPython WASM (python3).
88

99
## Commands
1010

@@ -257,6 +257,6 @@ Object.setPrototypeOf(MAP, null);
257257
- Always verify with `pnpm typecheck && pnpm lint:fix && pnpm knip && pnpm test:run` before finishing
258258
- Assert full stdout/stderr in tests, not partial matches
259259
- Implementation must match real bash behavior, not convenience
260-
- Dependencies using WASM are not allowed (exception: sql.js for SQLite, approved for security sandboxing)
260+
- No new WASM dependencies without approval. Approved exceptions: sql.js (SQLite), quickjs-emscripten (js-exec), and the vendored CPython WASM (python3); see THREAT_MODEL.md §4.7
261261
- We explicitly don't support 64-bit integers
262262
- All parsing/execution must have reasonable limits to prevent runaway compute

THREAT_MODEL.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ The following components are **trusted** and outside the scope of just-bash's ru
159159
| WeakRef/FinalizationRegistry | GC observation/side channels | Blocked by defense-in-depth proxy | `src/security/blocked-globals.ts` |
160160
| process.chdir() | Confuse CWD tracking | Blocked by defense-in-depth proxy | `src/security/blocked-globals.ts` |
161161
| **dynamic import()** | `import('/tmp/evil.js')` | **BLOCKED**: `Module._resolveFilename` blocks file specifiers; ESM loader hooks block `data:`/`blob:` URLs (Node.js 20.6+; see §4.1) | `src/security/defense-in-depth-box.ts` |
162-
| child_process | spawn/exec/fork | Not imported anywhere; no code path from interpreter | Architecture |
162+
| child_process | spawn/exec/fork | Not imported anywhere; no code path from interpreter. **With `javascript` enabled**, a *virtual* `child_process` module exists *inside* QuickJS (`js-exec-worker.ts:261–280`) whose `execSync`/`spawnSync` re-enter the sandbox via the SAB bridge, not the host OS — see §4.11 | Architecture (+ virtual shim, §4.11) |
163163

164164
### 3.6 Information Disclosure
165165

@@ -249,6 +249,8 @@ If any code captures a reference to `Function`, `eval`, etc. **before** the defe
249249

250250
**Mitigation**: Defense-in-depth is a secondary layer. The primary defense is that no code path exists from bash interpretation to JavaScript execution.
251251

252+
**Architectural invariant**: §4.2 and §4.3 both depend on a single invariant — **no bash→host-JS code path**: there must be no route by which an untrusted bash script can reach the host's `eval`, `new Function`, or dynamic `import()`, the only primitives that could turn these defense-in-depth gaps into a real escape. While that invariant holds, §4.2 and §4.3 stay LOW (defense-in-depth is secondary). If the invariant ever breaks, these residuals become **CRITICAL** — a pre-captured `Function` reference (§4.2) or a `globalThis.Function` reassignment (§4.3) would then be a direct JS-execution escape. The invariant is **guarded by the `check-banned-patterns` linter** (`scripts/check-banned-patterns.js`), which bans `eval(`, `new Function(`, and non-literal dynamic `import()` in source, so a path to host-JS code execution cannot be introduced silently. See §4.1 (three-layer `import()` mitigation) and §4.11 (the opt-in js-exec surface, the *only* intentional guest-JS execution path, which keeps the guest off the host's `eval`/`Function`).
253+
252254
### 4.3 globalThis Property Reassignment
253255

254256
**Risk**: LOW (defense-in-depth is secondary)
@@ -257,6 +259,8 @@ Attackers within the sandbox could overwrite `globalThis.Function` or use `Objec
257259

258260
**Mitigation**: Same as §4.2 — relies on no code path existing, not on the monkey-patching being unbypassable.
259261

262+
**Architectural invariant**: See §4.2 — relies on the same no-bash→host-JS-code-path invariant, which is guarded by the `check-banned-patterns` linter (`scripts/check-banned-patterns.js` bans `eval(`, `new Function(`, non-literal `import()`). Becomes **CRITICAL** if that invariant ever breaks.
263+
260264
### 4.4 Signal/Job Control Not Fully Modeled
261265

262266
**Risk**: LOW
@@ -328,6 +332,36 @@ Heredocs with variable expansion are size-limited (10MB) but nested heredocs wit
328332

329333
`Reflect` is frozen (not blocked) in the defense-in-depth layer. `Reflect.construct`, `Reflect.apply` etc. remain callable but cannot construct `Function` directly because the `Function` constructor itself is blocked.
330334

335+
### 4.11 JavaScript Execution Surface (When Enabled)
336+
337+
**Risk**: MEDIUM (intentional, opt-in, isolation by construction)
338+
339+
When `javascript: true` — or an `invokeTool` hook is provided, which implicitly enables js-exec (`src/Bash.ts`) — the `js-exec` and `node` commands execute untrusted JavaScript/TypeScript inside QuickJS (compiled to WASM via Emscripten) in a dedicated Worker thread. The `node` command is a stub that reroutes to js-exec; both surface the same boundary. Like Python (§4.7), this is an opt-in code-execution surface whose safety rests on isolation by construction rather than on a JS-level sandbox over the host.
340+
341+
**Enabling the surface**: js-exec is registered only when `options.javascript || jsConfig.invokeTool` (`src/Bash.ts`). It is absent from the command registry and unreachable from any bash script unless the host explicitly turns it on. The `invokeTool` hook enables js-exec implicitly because the hook is meaningless without it.
342+
343+
**Host bridge (SharedArrayBuffer)**: User code runs inside QuickJS, which has no Node.js APIs of its own. A synchronous SharedArrayBuffer protocol (`src/commands/worker-bridge/protocol.ts`) bridges selected host capabilities back into the guest:
344+
- **Virtual filesystem** — file read/write/stat are routed through the bridge to the in-memory OverlayFS, the same VFS the interpreter uses; no host-FS access.
345+
- **`exec`**`bridge-handler.ts` calls the host `exec` callback, which is `Bash.exec.bind(this)` (`src/Bash.ts`), i.e. it **re-enters the sandbox interpreter**, not the OS. Output is capped to the remaining execution deadline.
346+
- **`fetch`** — Web Fetch API, gated by the host `secureFetch` allow-list (off by default).
347+
- **`invokeTool`** — host-supplied tool hook for agent-driven tool calls; absent unless the host provides it.
348+
349+
**Virtual `child_process` shim**: Inside QuickJS, `import "child_process"` resolves to a **virtual module** (`js-exec-worker.ts:261–280`), not Node's `child_process`. Its `execSync`/`exec`/`spawnSync` route through `globalThis[Symbol.for('jb:exec')]` → the bridge → `Bash.exec`, so they re-enter the sandbox (no `spawn`/`fork`/OS process). This is the shim referenced from §3.5. Re-entrant js-exec is detected via AsyncLocalStorage and rejected to prevent deadlock ("recursive invocation is not supported").
350+
351+
**Isolation rests on**:
352+
- **QuickJS WASM** — untrusted JS executes in the QuickJS interpreter within WASM linear memory; the guest has no access to Node.js host objects, only the four explicitly bridged primitives above.
353+
- **Worker thread** — a dedicated worker per execution; terminated on timeout (`worker.terminate()`) and recycled after idle.
354+
- **`WorkerDefenseInDepth`** — activated after QuickJS loads with only `shared_array_buffer`, `atomics`, `process_stdout`, `process_stderr` excluded (SAB/Atomics are required by the sync bridge; stdout/stderr because Emscripten routes WASM output through Node's console).
355+
- **Memory cap** — QuickJS memory limited to 64MB (`MEMORY_LIMIT`, `js-exec-worker.ts`).
356+
- **Timeout** — 10s default, 60s when network/fetch is enabled (`DEFAULT_JS_TIMEOUT_MS` / `DEFAULT_JS_NETWORK_TIMEOUT_MS`, `js-exec.ts`); enforced by terminating the worker.
357+
358+
**Accepted behaviors** (not vulnerabilities):
359+
- `eval()`/`new Function()` inside QuickJS execute arbitrary *guest* JS — same posture as Python's `eval()`/`exec()` (§4.7); no host-JS escalation path because the guest cannot reach the host's `eval`/`Function`.
360+
- The bridge `exec` re-enters the sandbox, so any command reachable from js-exec is one the host already permitted in the bash environment; it does not widen the command surface beyond what bash itself can reach.
361+
- TS source is transpiled to JS before being handed to QuickJS; this is a guest-side convenience, not a host code path.
362+
363+
**Residual risk**: The security of this surface depends on the QuickJS/WASM boundary holding — i.e. the guest genuinely cannot reach host-JS primitives except through the four bridged calls above. The escape vectors to watch are a QuickJS/WASM breakout, or a bridge-call handler that acts on attacker-controlled arguments without validation (path validation in the FS bridge is delegated to the OverlayFs gates; `exec` re-enters the interpreter, which applies its own limits). Severity is **MEDIUM**, analogous to Python (§4.7): opt-in, isolated by construction, bounded by memory + timeout, and only as trustworthy as the WASM boundary it rests on. This surface is also the one residual that can weaken the no-bash→host-JS-code-path invariant of §4.2/§4.3 — but only if the WASM boundary is broken, since the guest is intentionally kept off the host's `eval`/`Function`/`import()`.
364+
331365
---
332366

333367
## 5. Defense Layer Summary

packages/just-bash/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Custom commands receive a `CommandContext` with `fs`, `cwd`, `env`, `stdin`, and
7070

7171
### Optional Runtimes
7272

73-
`js-exec` (JavaScript/TypeScript via QuickJS; requires `javascript: true`), `python3`/`python` (Python via CPython; requires `python: true`)
73+
`js-exec` (JavaScript/TypeScript via QuickJS; requires `javascript: true`), `python3`/`python` (Python via CPython; requires `python: true`). The `node` command is an alias of the `js-exec` runtime, not a real Node.js process.
7474

7575
### Compression & Archives
7676

@@ -594,7 +594,7 @@ All limits have defaults. Error messages tell you which limit was hit. Increase
594594

595595
## Browser Support
596596

597-
The core shell (parsing, execution, filesystem, and all built-in commands) works in browser environments. The following features require Node.js and are unavailable in browsers: `python3`/`python`, `sqlite3`, `js-exec`, and `OverlayFs`/`ReadWriteFs` (which access the real filesystem).
597+
The core shell (parsing, execution, filesystem, and all built-in commands) works in browser environments. The following features require Node.js and are unavailable in browsers: `python3`/`python`, `sqlite3`, `js-exec`/`node`, `tar`, `yq`, `xan`, and `OverlayFs`/`ReadWriteFs` (which access the real filesystem).
598598

599599
## Default Layout
600600

0 commit comments

Comments
 (0)