Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dc2883c
test(sandbox): reject .. escapes for nonexistent shell destinations
adriannoes Sep 14, 2026
ba1002d
fix(sandbox): walk existing ancestor before workspace prefix check
adriannoes Sep 14, 2026
a482524
test(sandbox): reject quoted and embedded absolute shell paths
adriannoes Sep 14, 2026
198330b
fix(sandbox): scan quoted and embedded absolute paths in allowlist
adriannoes Sep 14, 2026
8f90fec
test(sandbox): reject $HOME and $PWD expansions in workspace allowlist
adriannoes Sep 14, 2026
d9d1a9a
fix(sandbox): expand $HOME and $PWD before workspace allowlist checks
adriannoes Sep 14, 2026
6f4b3b5
test(sandbox): reject glued $IFS and mid-token $HOME expansions
adriannoes Sep 14, 2026
79bc0c6
fix(sandbox): scan $ expansions on the full shell command
adriannoes Sep 14, 2026
590247e
docs(sandbox): record workspace_only glued-$ gate and residuals
adriannoes Sep 14, 2026
7126bd9
test(sandbox): reject missing-dir .., file: URLs, and embedded ../
adriannoes Sep 14, 2026
9bdec30
fix(sandbox): collapse .. lexically and scan file: plus embedded ../
adriannoes Sep 14, 2026
76ccc1b
fix(sandbox): zero-init lexical collapse segment pointers
adriannoes Sep 14, 2026
16a643c
test(sandbox): reject encoded slash, HOME/PWD assign, symlink .., file %
adriannoes Sep 14, 2026
a39c6a4
fix(sandbox): block encoded slash, HOME/PWD assign, symlink .., file %
adriannoes Sep 14, 2026
264a059
fix(sandbox): narrow encoded-slash octal locals for cppcheck
adriannoes Sep 14, 2026
c6798b3
test(sandbox): reject quoted HOME/PWD assign, split file:, encoded dot
adriannoes Sep 14, 2026
185d165
fix(sandbox): catch quoted HOME/PWD, split file:, encoded dot/N
adriannoes Sep 14, 2026
925a0dc
fix(sandbox): narrow encoded-dot hex local for cppcheck
adriannoes Sep 14, 2026
d568e1c
test(sandbox): reject URL ../, Perl \\x{}, env -i/pop, comma HOME=
adriannoes Sep 14, 2026
65e375d
fix(sandbox): catch URL ../, Perl \x{}, env -i/pop, comma HOME=
adriannoes Sep 14, 2026
0dfeecf
test(sandbox): reject read/unset env, Perl \o{}, encoded file:
adriannoes Sep 14, 2026
beae45a
fix(sandbox): catch read/--unset/-iu, Perl \o{}, encoded file:
adriannoes Sep 14, 2026
311d70e
test(sandbox): reject octal/identity file:, \\+nl, printf -v
adriannoes Sep 14, 2026
2d0fc3e
fix(sandbox): catch octal/identity file:, \\+nl, printf -v
adriannoes Sep 14, 2026
971d64f
test(sandbox): reject identity HOME/PWD, nameref, encoded $
adriannoes Sep 14, 2026
3f6528f
fix(sandbox): fold identity before HOME/PWD; decode encoded $
adriannoes Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha
## [Unreleased]

### Fixed
- Shell `workspace_only` walks to the first existing ancestor instead of a lexical prefix, so a missing `workspace/../../tmp/stolen` destination cannot escape the sandbox.
- Shell `workspace_only` scans quoted and embedded absolute paths (`cat '/etc/passwd'`, `python3 -c "open('/etc/passwd')"`) and fail-closes on `strdup` OOM. Relative tokens and URL slashes stay allowed; `file:///...` is still blocked.
- Shell `workspace_only` expands `$HOME` / `${HOME}` / `$PWD` / `${PWD}` (including one quote layer) before the workspace check and fail-closes other `$...` forms such as ANSI-C `$'\x2f...'`. Glued expansions (`cat$IFS/etc/passwd`, `cat${IFS}/...`, `cat"$HOME/..."`, `python3 -c "open('$HOME/...')"`) are scanned on the full command, not only strtok tokens that start with `$`. `file:` URL variants (`file:/`, `file://localhost/`, `file://etc/passwd`) are extracted (including quote-split `f'ile:` / `'f'+'ile:`, hex/unicode-hidden `\x66ile:` / `\u0066ile:`, octal `\146ile:` / `\072`, and identity `f\ile:`) and percent-decoded (`%2e%2e`, `%2f`) before the workspace check. POSIX `\` + newline (optional CR) is collapsed before those scans. Embedded relative `../` is checked against the workspace, including `../` after `://` (`https://example.com/../../../../etc/passwd`); real `https://` fetches without a `..` walk stay allowed. `..` is collapsed lexically so a missing directory before `..` cannot pin the ancestor walk at the workspace, and is not cancelled across a symlink. Encoded `/` and `.` (`\x2f`, `\x2e`, `\x{2f}`, `\57`, `\56`, `\u002f`, `\u{2f}`, `\o{57}`) reconstruct a path body; `\N{` fail-closes. In-command `HOME=` / `PWD=` assignment, `export`, and `unset` fail closed even inside `eval` / `sh -c` quotes or after a comma or `[`. `env -i`, clustered `env -iu`, `env -u` / `--unset` HOME|PWD, POSIX `read HOME|PWD`, `printf -v HOME|PWD`, `declare -n` targeting HOME|PWD, `exec -c`, and `os.environ.pop`/`del`/`clear`/`update` / `os.unsetenv` / `os.putenv` / `os.environ["HOME"]=` of those names fail closed instead of trusting process getenv. Identity-escape fold (same decode as `file:` recovery) runs before the HOME/PWD keyword gate so `export PW\D=` / `\unset HOME` / `\env -i` cannot skip getenv. Encoded `$` (`\x24` / `\044` / `\u0024`) is decoded before the `$` expansion scan.
- Discord Gateway RX grows for the trailing NUL so two 64 KiB libwebsockets fragments cannot write one byte past the heap block (typical READY payloads).
- WebChat inbound WS `rx_buffer_size` is `WS_RX_BUFFER_SIZE` (`WS_TEXT_MAX` plus JSON envelope) so dashboard messages are not split across 256-byte RECEIVE callbacks and dropped.
- WebChat WebSocket sends now accept agent replies up to 32 KiB (`WS_TEXT_MAX`, matching `RESPONSE_BUF_SIZE`) instead of silently dropping payloads above 8 KiB. Dest buffers are `WS_TEXT_BUF_SIZE` so a max-length payload keeps its NUL; a too-large frame is logged instead of skipped with `<`.
Expand Down
4 changes: 2 additions & 2 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The primary goals are: prevent sandboxed shell commands from escaping to host de
|---------|-----------|-------|
| Shell (sandbox on) | `fork()` + `unshare(CLONE_NEWNS \| CLONE_NEWNET \| CLONE_NEWPID)` + `prctl(PR_SET_NO_NEW_PRIVS)` | See [Linux sandbox (Jetson)](#linux-sandbox-jetson) |
| Shell (sandbox off) | Plain `fork()` + substring fallback blocklist | **Not** a security boundary; stderr warning |
| Allowlist | Substring blocklist + optional workspace `realpath` containment | Defense in depth before `sandbox_exec` |
| Allowlist | Substring blocklist + workspace containment (existing ancestor, quoted/embedded `/` `~`, full-command `$HOME`/`$PWD` / fail-closed `$`) | Primary host-FS gate; namespaces are not a chroot |
| cgroups v2 | `memory.max`, `cpu.max` on child PID | Best-effort; non-fatal if cgroup write fails |
| Hardware GPIO/I2C | libgpiod / `i2c-dev` in agent process | Not exposed inside shell namespace |

Expand Down Expand Up @@ -73,7 +73,7 @@ The task checklist references `unshare(CLONE_NEWNS) + pivot_root` as a hardened

**Mitigation in v1.0:** the shell allowlist rejects commands whose text references `/dev/nvhost`, `/dev/nvgpu`, or `/dev/nvmap` (substring blocklist). Regression tests live in `tests/test_allowlist.c` (`test_block_jetson_gpu_devices`).

**Residual risk:** a crafted command that opens GPU nodes without those literal substrings (e.g. shell globs or indirect paths) may still reach devices until a future release adds mount-slave propagation, a minimal `/dev` tmpfs, or seccomp. Track as post-v1.0 hardening.
**Residual risk:** a crafted command that opens GPU nodes without those literal substrings (e.g. shell globs) may still reach devices until a future release adds mount-slave propagation, a minimal `/dev` tmpfs, Landlock, or seccomp. `workspace_only` walks a missing destination’s existing ancestor, collapses `..` lexically (including a missing directory before `..`, without cancelling `..` across a symlink), scans quoted/embedded `/` `~` and relative `../` (including `../` after `://`), extracts and percent-decodes `file:` URLs (including quote-split schemes, `\x66`/`\u0066`/`\146` hidden schemes, identity `f\ile:`, and POSIX `\`+newline continuation), reconstructs encoded `/` and `.` (`\x2f` / `\x{2f}` / `\x2e` / `\u{2f}` / `\o{57}`), fail-closes `\N{` and in-command `HOME`/`PWD` assignment (including quoted `eval` / `sh -c`, comma-separated argv, POSIX `read`, `printf -v`, `declare -n` targeting HOME|PWD, `exec -c`, `env -i` / `-iu` / `-u` / `--unset`, and `os.environ.pop`/`clear`/`update` / `os.unsetenv` / `os.putenv` / subscript assign; identity-escape fold before the keyword gate so `PW\D=` / `\unset` / `\env -i` cannot skip getenv), and expands or fail-closes `$` on the full command (including glued `$IFS`, mid-token `$HOME`, and encoded `\x24` / `\044` / `\u0024`). Still out of this gate: relative tokens after `cd` with no `.` `/` `~` `$` (Landlock cluster), Python `open(chr(47)+'etc/passwd')` (no path character or slash encoding in the command string), and conservative regex false positives such as `awk '/foo/'`.

### Board-agnostic blocklist entries (Jetson literals)

Expand Down
Loading
Loading