From 48c884a53387377c29f88abff7859d25815a5326 Mon Sep 17 00:00:00 2001 From: Aling33 <152494711+Aling33@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:08:17 +0800 Subject: [PATCH] fix(sandbox): treat a fetched process substitution as an interpreter program (#7290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sandbox): treat a fetched process substitution as a program operand `bash <(curl url)` hands the fetched file to bash as an *operand*, not as a redirect: bash runs it. The audit only recognised the stdin spelling (`bash < <(curl url)`), so the operand spelling executable-fetched content through the sandbox audit unimpeded — the miss documented in the middlewares AGENTS.md known-gaps list. An interpreter in command position whose first operand is a risky process substitution now blocks. The two exceptions mirror the stdin rule: a script operand in front (`bash script.sh <(curl url)`) keeps the substitution an ordinary argument, and a shell noexec flag (`bash -n <(curl url)`) only parses it. Verified in Git Bash: bash <(echo "echo FETCHED_AS_PROGRAM") -> FETCHED_AS_PROGRAM bash <(echo "echo FETCHED_WITH_ARG") consumer.sh -> FETCHED_WITH_ARG bash -n <(echo "echo NOEXEC") -> exit 0, no output * docs(sandbox): clarify substitution audit limitations --------- Co-authored-by: Willem Jiang --- .../deerflow/agents/middlewares/AGENTS.md | 23 +++++++++++-- .../middlewares/sandbox_audit_middleware.py | 33 ++++++++++++++++--- .../tests/test_sandbox_audit_middleware.py | 18 ++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md index 9b491b5523..a92ff376fa 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md +++ b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md @@ -91,10 +91,29 @@ No match: skip inspection, log ID. Tests: `test_read_mark_tool_call_correlation. `__authorization_outcome` key (so `build_run_config` strips caller-supplied forgeries). Consumers pop it; the publisher and the consumer share only that contract module. -12. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations before tool execution; command classification is **defense-in-depth and audit, not a security boundary** (the sandbox is the isolation boundary). Command substitution is judged by *position*, not the presence of `$(`: **command position** (`$(curl url)`, `` `curl url` ``, the word after `|`/`&&`/`;`, an `eval`/`source` argument, a command-position `.`) executes fetched content and is blocked; **value position** (`x=$(curl url)`, `echo $(curl url)`, an argument, a `for` word list) only captures output and passes (#4611). So `_HIGH_RISK_COMMAND_POSITION_PATTERNS` is matched anchored against each sub-command from `_split_compound_command(split_pipes=True)`, never the whole string; pipe-spanning rules (`| sh`, `base64 -d | ...`) still use `_classify_command`'s whole-command Pass 1. `_COMMAND_POSITION_PREFIX` extends the anchor over leading assignments and exec wrappers (`FOO=1 $(curl url)`, `env`/`command`/`builtin`/`exec`/`nohup`/`time`/`sudo`/`doas`); its assignment branch requires whitespace before the substitution, which keeps `x=$(curl url)` in value position. Two contexts are deliberately **position-blind** (matched whole-command in Pass 1, since they execute their input anywhere, e.g. `xargs sh -c "$(curl url)"`): an `eval`/`source` argument (including one that reaches the dot builtin, `eval . $(curl url)`), and an interpreter **code-string flag** — `-c` (shells, `python`), `-e` (`perl`/`ruby`/`node`), `-p` (`perl`/`node`), `-r` (`php`) — plus the here-string (`<<<`) reaching the same place via stdin. An interpreter that reads its program from stdin (`bash < <(curl url)`) is that same execution context, decided from the token list rather than one pattern: a script operand anywhere (`bash script.sh < <(curl url)`, `bash < <(curl url) consumer.sh`) or a shell noexec flag (`bash -n < <(curl url)`) keeps the fetched bytes as data, and `.`/`source` never read a program from stdin because they require a filename. All three substitution spellings (`$(`, `<(`, `` ` ``) share one `_RISKY_SUBSTITUTION` opener. An unquoted newline splits like `;` (else `echo hi\n$(curl url)` evades the anchored rules). Heredoc bodies are data: `_split_compound_command` records headers (`<