Phase A -- Shell containment¶
Status: Implemented (2026-07-23); follow-up hardening (2026-07-23)
Goal¶
Close critical shell jail bypasses in restricted mode so file-reading commands cannot reach host paths via shell expansion or flag-value indirection, with adversarial tests that fail CI on regression.
Follow-up hardening (2026-07-23)¶
Adversarial recheck found residual CRITICAL gaps after initial Phase A:
| ID | Finding | Fix |
|---|---|---|
| SEC-SHELL-03 | Quoted paths -- cmd.split() leaves quotes on tokens; jail treats "/etc/passwd" as a weird relative path inside workspace while the shell strips quotes and reads the host |
Tokenize with shlex.split(posix=True) before path validation; reject invalid quoting |
| SEC-SHELL-04 | Attached short-flag paths -- -o/tmp/out, -f/etc/hosts skipped because the whole token is not a known flag |
_match_flag_with_value() splits attached forms for path-taking flags |
| SEC-SHELL-05 | jq -L library path -- -L was in skip list; outside modules loadable |
Treat -L / --library-path as path-value flags (including -L/path) |
Adversarial coverage: TestQuotedPathBypass, TestAttachedFlagPathBypass in tests/adversarial/test_shell_sandbox.py.
Second adversarial recheck (2026-07-23) found residual CRITICAL gaps in sort:
| ID | Finding | Fix |
|---|---|---|
| SEC-SHELL-06 | sort --files0-from -- list file is jailed but NUL-separated paths inside it are not; workspace list containing /etc/passwd\0 reads host file |
Reject --files0-from entirely in restricted mode |
| SEC-SHELL-07 | sort --compress-program -- flag was in skip list; workspace script executes arbitrarily (RCE) |
Reject --compress-program entirely in restricted mode |
-T / --temporary-directory remain skipped (not jailed): sort may write temp files outside workspace (data-leak tradeoff); rejecting would break sort on non-writable workspaces.
Adversarial coverage: TestSortDangerousFlags in tests/adversarial/test_shell_sandbox.py.
Why (problems addressed -- bullet list with severity)¶
- CRITICAL:
$HOME/~expansion -- jail validates literal tokens (~/.ssh/...) butasyncio.create_subprocess_shellexpands tildes at execution time (src/hive/tools/shell/toolkit.py_check_paths_in_commandvsshell_exec). - CRITICAL: Flag-value paths --
sort -o /etc/passwd,grep -f /etc/passwdskip path validation because value tokens are excluded from_extract_path_args(_SORT_VALUE_FLAGS,_grep_path_argswithout-fhandling). - HIGH (adjacent):
grep --file,sort --files0-from,cut -ffield specs vs paths -- similar parser gaps. - MED: Residual dev-command RCE when
tools.shell_allow_dev_commands: true-- documented, not removed (opt-in tier).
Related issues bundled¶
| ID | Finding |
|---|---|
| SEC-SHELL-01 | $HOME / ~ / $VAR path expansion bypass |
| SEC-SHELL-02 | sort -o, grep -f, --output, --files0-from |
| SEC-SHELL-03 | Adversarial coverage gaps in tests/adversarial/test_shell_sandbox.py |
Current state (files)¶
| Area | Location | Behavior today |
|---|---|---|
| Path jail | src/hive/tools/shell/toolkit.py |
Resolves (workspace / arg).resolve() on raw tokens; no expanduser / env expansion |
| Subprocess | same file shell_exec() |
create_subprocess_shell(command, cwd=workspace, env HOME=workspace) |
| Flag parsing | _grep_path_args, _flagged_path_args, _SORT_VALUE_FLAGS |
Output/pattern file flags excluded from validation |
| Tests | tests/adversarial/test_shell_sandbox.py |
Covers absolute paths, ../, many read commands; no tilde/env/-o/-f cases |
| Docs | docs/hardening-guide.md Change 3 |
Claims path jail on SAFE_COMMANDS |
Proposed changes (numbered)¶
- Reject expansion-prone tokens before execution in
_check_command()or_check_paths_in_command(): - Reject args containing
~,$,`, or%(Windows-style) in path positions. -
Reject bare
$HOME,${HOME}, etc. even when jail would treat them as relative paths. -
Validate flag-value paths, not only positional args:
- Extend
_grep_path_argsto treat-f,--file,--exclude-fromvalues as paths requiring workspace containment. - Extend
_flagged_path_argsforsortto validate-o/--output/--files0-fromtargets (still skip-Ttemp dir if it must stay internal -- document policy). -
Audit
cut,diff,jqfor similar flags. -
Optional hardening (choose one, document tradeoff):
- A (preferred for restricted mode): Replace
create_subprocess_shellwithcreate_subprocess_exec+ argv parsing for allowlisted commands (no shell expansion). -
B (minimal): Keep shell but add pre-pass that expands/rejects using
os.path.expanduser+ explicit env map (onlyHOME=workspaceallowed). -
Adversarial tests in
tests/adversarial/test_shell_sandbox.py: cat ~/.ssh/id_rsa,cat $HOME/.hive/hive.db,grep -f /etc/passwd x,sort -o /tmp/out lines.txt.-
Regression: workspace-relative reads still pass (
cat test.txt,sort -o out.txt lines.txt). -
Docs: Update
docs/hardening-guide.mdshell section +docs/guide/toolkits.mdshell table with explicit "no tilde/env paths in restricted mode".
Non-goals¶
- Disabling
allow_dev_commandstier (already defaultFalseinsrc/hive/config.py). - Container / Landlock / seccomp sandbox (deployment concern).
- Rewriting allowlist to remove
sort,grep, etc.
Risks / rollback¶
| Risk | Mitigation |
|---|---|
Legitimate agent uses ~/workspace/foo in scripts |
Document workspace-relative paths only; error message suggests foo or ./foo |
sort -o out in workspace breaks if over-validated |
Tests for allowed -o inside workspace |
| argv parsing drift vs shell | Restrict to SAFE_COMMANDS set; dev tier keeps shell |
Rollback: revert validation-only changes; keep new tests skipped with @pytest.mark.xfail only as temporary measure (prefer fix forward).
Acceptance criteria (testable)¶
uv run pytest tests/adversarial/test_shell_sandbox.py -v --tb=short
uv run pytest tests/runtime/test_web_tools.py -q # no shell regressions
- [x]
ShellToolkit(restrict=True, allow_dev_commands=False).shell_exec("cat ~/.ssh/id_rsa")returns error before subprocess (or empty safe output), never host file contents. - [x]
sort -o /etc/passwd ws.txtandgrep -f /etc/passwd ws.txtblocked with workspace escape message. - [x] Existing
TestWorkspaceJailtests remain green. - [x]
docs/hardening-guide.mddocuments tilde/env and flag-value policy.
Suggested implementation order¶
- Add failing adversarial tests (tilde,
$HOME,-o,-f). - Token rejection for expansion metacharacters.
- Flag-value path validation helpers (shared with
_flagged_path_args). - Optional: subprocess_exec path for SAFE_COMMANDS.
- Docs pass.
Estimate¶
M (2--3 days): parser edge cases + tests + doc update.
Dependencies (prior phases)¶
None. Should land before broader agent-loop phases so CI stays trustworthy.