Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECU
- **FD-based API key handoff** (`cmd/odek/subagent_key.go`) — parent writes key to a 0600 tempfile, immediately `unlink()`s, passes the FD via `cmd.ExtraFiles`. Sub-agent reads from `$ODEK_API_KEY_FD` and closes. Key never in `/proc/<pid>/environ`.
- **Approver friction** (`internal/danger/approver.go`, `cmd/odek/wsapprover.go`, `cmd/odek/shell.go`, `cmd/odek/perf_tools.go`) — both TTYApprover and WSApprover engage friction mode after 3 approvals of the same class in 60s: require typing literal `approve`, 1.5s pause. Trust-class shortcut disabled for `destructive` + `blocked` regardless. CLI `shell`/`parallel_shell` reuse a single TTYApprover instance per process so the counter and trust cache persist across prompts.
- **Danger classifier bypass resistance** (`internal/danger/classifier.go`) — `normalize()` pre-processes: expand `$IFS` / `${IFS}`, extract `$(...)` / `` `...` `` substitutions, strip `command` / `exec` / `builtin` wrappers, collapse unquoted backslashes, basename absolute paths. `awk`/`gawk`, `sed` (`e` command / `-f`), and editors (`vi`/`vim`/`nvim`/`emacs`/`ed`/`ex`) are classified as `code_execution` when given a script or file operand, closing `awk 'BEGIN{system(...)}'`, `sed 's///e'`, and editor `!` shell escapes. `rm -rf ./` / `rm -rf ./..` are normalised to `.` / `..` for wipe-target matching, and `${VAR:--rf}` default-value flag substitutions are treated as fail-closed. Regression suite in `classifier_bypass_test.go`.
- **Pipe-fed xargs composition** (`internal/danger/classifier.go`) — when a pipeline's sink is `xargs` invoking a destructive/system verb, statically determinable upstream payloads (`echo`/`printf` args) are composed onto the inner command before classification (`echo "/" | xargs rm -rf` → `destructive`, like `rm -rf /`); when the payload is not statically determinable (`cat file | …`, `find … | …`, `$VARS`) and the inner verb is dangerous-capable (`rm`, `shred`, `dd`, `chmod`, `chown`, `mkfs.*`, …), the pipeline fails closed as `unknown` (deny-by-default).
- **Root-level mutation targets** (`internal/danger/classifier.go`) — `ClassifyPath("/")` is `system_write`, so `chmod -R 777 /`, `chattr -R +i /`, and `mv / /tmp/x` prompt instead of falling through to auto-allowed `local_write` (parity with `chown -R`). `chattr` is handled with the same operand scan as `chmod`.
- **Git data-loss verbs gated** (`internal/danger/classifier.go`) — `git clean -f*` (unless `-n`/`--dry-run`), `git reset --hard`, `git checkout -f`/`--`/`./…`, `git restore` (worktree), `git branch -D` / `--delete --force`, `git stash drop`/`clear`, and `git reflog expire` classify as `system_write` (prompt-by-default) instead of `safe`, so an injected payload cannot wipe a working tree with zero friction.
- **WebSocket CSRF token** (`cmd/odek/serve.go`) — `odek serve` issues a random 256-bit token at startup and requires it on `/ws` via cookie, `X-Odek-Ws-Token` header, or `odek.<token>` subprotocol. The token is no longer embedded in every `GET /` response; it is only delivered when the request includes the correct `?token=` query parameter (Jupyter-style), and a non-loopback bind prints a loud warning. The localhost origin check remains as defense-in-depth.
- **SSRF / DNS-rebinding dial guard** (`cmd/odek/ssrf_guard.go` + `internal/danger/classifier.go`) — `browser`, `http_batch`, and `web_search` resolve hostnames at dial time and refuse internal IPs (loopback, RFC1918, RFC4193, link-local, RFC 6598 CGNAT `100.64.0.0/10`, RFC 2544 benchmark `198.18.0.0/15`, and unspecified), then pin the dial to the validated IP. Operator-configured backends (e.g. `web_search.base_url`) are added to an allowlist so container-internal services such as SearXNG remain reachable.
- **SSRF guard proxy refusal** (`cmd/odek/ssrf_guard.go`) — when `HTTP(S)_PROXY` is set, the transport would dial the proxy instead of the target and the dial guard would only validate the proxy address. `ssrfGuardedTransport` detects an active proxy, logs a warning, and refuses the request so SSRF protection is not silently voided.
Expand Down
4 changes: 2 additions & 2 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,12 @@ When running without `--sandbox`, odek classifies every shell command by risk an
|-------|---------|----------|
| 🟢 safe | allow | `ls`, `cat`, `grep`, `go build` |
| 🟡 local_write | allow | `rm file`, `mv`, `echo > file` |
| 🟠 system_write | **prompt** | `sudo`, `apt install`, writes to `/etc/` |
| 🟠 system_write | **prompt** | `sudo`, `apt install`, writes to `/etc/`, `chmod -R 777 /`, `git reset --hard`, `git clean -fdx` |
| 🔴 destructive | **deny** | `rm -rf /`, `dd if=/dev/zero`, `mkfs` |
| 🔴 network_egress | **prompt** | `curl`, `git push`, `ssh`, `scp` |
| 🔴 code_execution | **prompt** | `curl url \| bash`, `eval`, `node -e`, `go run` |
| 🟠 install | **prompt** | `npm install`, `pip install`, `go install <path>` |
| 🔴 unknown | **deny** | any command whose program name isn't recognised; MCP tools (`<server>__<tool>`) |
| 🔴 unknown | **deny** | any command whose program name isn't recognised; MCP tools (`<server>__<tool>`); pipe-fed `xargs <verb>` whose stdin payload isn't statically determinable |
| ⬛ blocked | **deny** | Fork bombs, `dd` to block devices |

odek **fails closed**: a command or MCP tool whose name matches no known-safe or known-dangerous
Expand Down
3 changes: 3 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ The classifier is hardened against common evasion tricks (see the package doc in
- `git -c alias.x='!id' x`, `git -c core.pager='sh -c id' --paginate log`, `git config --global alias.pwn '!cmd'` — `git -c` / `--config-env` overrides and the `git config` subcommand are `code_execution` because they can define arbitrary shell commands.
- `find . -delete`, `rsync -a --delete /empty/ ~`, `rsync --remove-source-files` — bulk-deletion flags are `destructive`; `find -fprint` / `-fprintf` are `local_write` because they write match lists to arbitrary files.
- `echo x >> ~/.bashrc`, `cp evil ~/.profile`, `dd if=evil of=~/.bashrc` — shell file operands and redirect targets are run through `ClassifyPath`, so writes to shell rc files, `~/.ssh`, `~/.odek` trust anchors, and other home-sensitive paths are `system_write` instead of auto-allowed `local_write`. Matching is case-insensitive so variants such as `~/.BASHRC` or `~/.odek/CONFIG.JSON` are escalated on case-insensitive filesystems.
- `echo "/" | xargs rm -rf` — a pipeline whose sink is `xargs` composing a destructive/system verb has its upstream literal payload (`echo`/`printf` arguments) composed onto the inner command before classification, so pipe-fed `xargs rm -rf` classifies exactly like `rm -rf /` (`destructive` → deny). When the payload is not statically determinable (`cat file | …`, `find … | …`, `$VARS`) and the inner verb can turn a piped path into destructive/system damage (`rm`, `shred`, `dd`, `chmod`, `chown`, `mkfs.*`, …), the pipeline fails closed as `unknown` (deny-by-default).
- `chmod -R 777 /`, `chattr -R +i /`, `mv / /tmp/x` — the filesystem root itself classifies as `system_write` in `ClassifyPath`, so recursive permission/attribute flips or moves aimed at `/` prompt instead of falling through to auto-allowed `local_write`. `chattr` is now handled with the same operand scan as `chmod`.
- `git clean -fdx`, `git reset --hard`, `git checkout -- .`, `git restore .`, `git branch -D`, `git stash drop`/`clear`, `git reflog expire` — irreversible git data-loss verbs are `system_write` (prompt-by-default) instead of `safe`, so a prompt-injection payload cannot wipe a working tree with zero friction. Dry-run and non-destructive forms (`git clean -n`, `git checkout main`, `git branch -d`, `git stash pop`, `git restore --staged`) stay `safe`.

Regression suites (`internal/danger/classifier_bypass_test.go` and `hardening_test.go`) pin these as known-closed evasions. If you find a new bypass, those test files are the place to add it.

Expand Down
Loading
Loading