fix(#6541): skip SSRF validation for inert pipelines - #6542
fix(#6541): skip SSRF validation for inert pipelines#6542fullsend-ai-coder[bot] wants to merge 1 commit into
Conversation
|
🤖 Finished Review · ✅ Success · Started 8:56 PM UTC · Completed 9:14 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.79 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ReviewFindingsLow
Previous runReviewFindingsHigh
Medium
Low
Next steps:
Previous run (2)ReviewFindingsMedium
Low
Next steps:
Previous run (3)ReviewFindingsMedium
Low
Next steps:
Previous run (4)ReviewFindingsCritical
High
Medium
Low
Labels: PR modifies SSRF security hook in internal/security/hooks/ Next steps:
|
|
🤖 Finished Fix · ✅ Success · Started 9:16 PM UTC · Completed 9:24 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.55 |
🔧 Fix agent — iteration 1 (bot-triggered)Addressed 4 of 7 review findings by removing unsafe commands (sed, awk, gawk, mawk, nawk, tee) from _INERT_COMMANDS and adding /dev/tcp and /dev/udp detection. Disagreed with 3 findings: authorization-tier (process concern), scope-boundary (awareness item), and code-organization (style refactor out of scope). All 82 tests pass. Fixed (4):
Disagreed (3):
Tests: passedNext steps:
|
Address review findings on PR #6542: - Remove sed from _INERT_COMMANDS (GNU sed 'e' command/flag executes shell commands) - Remove awk, gawk, mawk, nawk from _INERT_COMMANDS (system(), getline-from-pipe, and gawk has native TCP/UDP networking) - Remove tee from _INERT_COMMANDS (writes to arbitrary files) - Add /dev/tcp and /dev/udp detection in process_tool_call to block Bash virtual network device redirections unconditionally - Update tests: remove sed/awk inert assertions, add security regression tests for all bypass vectors Addresses review feedback on #6542
|
🤖 Finished Review · ✅ Success · Started 9:26 PM UTC · Completed 9:45 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.83 |
|
🤖 Finished Fix · ✅ Success · Started 9:47 PM UTC · Completed 9:55 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.42 |
🔧 Fix agent — iteration 2 (bot-triggered)Addressed 4 of 8 review findings with code fixes: hardened _DEV_NET_PATTERN against shell-quote evasion, added subprocess-based integration tests, broadened _SHELL_REENTRY for combined flags, and fixed _extract_base_command empty-string contract violation. Disagreed with 4 findings: 2 are acceptable conservative behavior (not security gaps), 1 is a coordination note, and 1 is a process concern for human maintainers. Fixed (4):
Disagreed (4):
Tests: passedNext steps:
|
- Make _DEV_NET_PATTERN quote-aware to catch /dev/'tcp'/HOST/80 and /dev/"tcp"/HOST/80 evasion (Bash strips quotes before processing virtual device paths) - Broaden _SHELL_REENTRY regex to catch combined flags (bash -lc, sh -xc, bash --norc -c) as defense-in-depth - Fix _extract_base_command to return None instead of empty string when path ends in trailing slash, matching documented contract - Add subprocess-based integration tests (TestSubprocessProtocol) following the _run_hook() convention used by other hook test files - Add unit tests for all new edge cases Addresses review feedback on #6542
|
🤖 Finished Review · ✅ Success · Started 9:57 PM UTC · Completed 10:16 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.41 |
|
🤖 Finished Fix · ✅ Success · Started 10:18 PM UTC · Completed 10:26 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.52 |
🔧 Fix agent — iteration 3 (bot-triggered)Fixed 2 medium-severity findings: (1) hardened /dev/tcp detection against partial-quoting and variable-expansion evasion by stripping quotes before matching and flagging /dev/$ patterns, (2) added double-quote context tracking to _has_command_substitution() to prevent single quotes inside double quotes from hiding command substitutions. Disagreed with 4 low-severity informational findings (conservative false positives, PR coordination, label ambiguity). All 105 tests pass. Fixed (2):
Disagreed (4):
Tests: passedNext steps:
|
…RF hook - Replace _DEV_NET_PATTERN regex with _has_dev_net_device() that strips all quotes before matching, closing partial-quoting evasion (e.g. /dev/tc'p'/HOST/80). Also flag /dev/$ for variable-expansion evasion (e.g. /dev/$PROTO/HOST/80). - Add double-quote tracking to _has_command_substitution() so single quotes inside double quotes are treated as literal, preventing hidden command substitutions (e.g. echo "'"$(curl ...))"'"). - Add 12 new tests covering partial quoting, variable expansion, double-quote context, and process-substitution-inside-dq semantics. Addresses review feedback on #6542
|
🤖 Finished Review · ✅ Success · Started 10:28 PM UTC · Completed 10:47 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.64 |
|
🤖 Finished Fix · ❌ Failure · Started 10:49 PM UTC · Completed 11:01 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.81 |
|
The fix agent completed, but the post-fix script failed before finishing. Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32671662930 Details: |
URL literals passed as data to non-network commands (echo/printf/cat piped into cut/grep/jq, etc.) were blocked because URL_PATTERN matched them and validate_url did a fail-closed DNS lookup in the sandbox. Skip validation when every stage of a Bash pipeline is a known-inert command. The exemption is fail-closed and deliberately narrow: - _INERT_COMMANDS enumerates ~45 commands with no network capability; sed/awk (shell execution via GNU e / system()), tee/xargs/find (write or exec), read/yes, and every interpreter or shell are excluded, so an unknown command falls through to full validation. - Shell reentry (bash -c/-lc clusters, eval, exec), command and process substitution, /dev/tcp and /dev/udp devices (including quote-, backslash-, and ANSI-C-obscured forms), and variable-assembled redirection targets each defeat the exemption. - The inert check runs only after a URL survives the existing pattern-context filter, so URL-free commands pay no extra parsing. Complements #6536, which exempts URLs inside sed/grep/awk pattern arguments; this exempts URLs that are data in a fully non-network pipeline. The two paths are independent and both narrowing/guarded. Residual, by design: a /dev/tcp path assembled purely from shell variables with no literal URL is not blocked here (no URL to validate); the sandbox network policy remains the enforcing layer, this hook is defense-in-depth. Assisted-by: Claude (fix) Signed-off-by: Wayne Sun <gsun@redhat.com>
675510e to
f6843d5
Compare
|
Thanks — all 14 findings addressed in the rebased commit ( Fixed by removing from
Fixed in
Fixed in substitution detection:
Fixed as convention:
Accepted as conservative over-blocking (not security gaps), documented in the PR body's Residual section:
Verified: 223 tests in |
|
🤖 Finished Review · ✅ Success · Started 12:17 PM UTC · Completed 12:59 PM UTC Commit: Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high |
Superseded by updated review
Summary
Skip SSRF URL validation in
ssrf_pretool.pywhen the entire Bash command pipeline consists of known-inert (non-network) commands. This fixes false-positive blocks on commands likeecho URL | cut -d/ -f4,5where URLs are text data, not outbound targets — the shape #6535/#6536 identified as still failing.Related Issue
Refs #6541 (the sed-stage repros in that issue deliberately still fall through — see below).
Changes
_INERT_COMMANDSfrozenset (~45 commands that cannot make network requests: echo, printf, cat, grep, cut, tr, jq, sort, base64, sha256sum, …)._pipeline_is_inert(): fail-closed check that returnsTrueonly when every pipeline stage is inert and there is no substitution, shell reentry,/dev/tcp-style device access, or variable-assembled redirection target._has_command_substitution(): quote-aware detector for$(), backticks, and process substitution<()/>(). Tracks double-quote context so an apostrophe inside double quotes cannot open a phantom single-quote region and hide a later substitution._split_command_stages(): quote-aware splitter for|,;,&&,||,&, and newlines._extract_base_command(): extracts the command name from a stage, skipping variable assignments and path prefixes._has_dev_net_device()+_strip_shell_quoting(): detect/dev/tcpand/dev/udpincluding quote-, backslash-, and ANSI-C-obscured forms (/dev/tc'p'/,/dev/\tcp/,/dev/tc$'\x70'/) and any variable expansion inside a/dev/path. Blocked outright inprocess_tool_call(), before the inert bypass.process_tool_call(), run the inert check only after a URL survives the existing pattern-context filter — URL-free commands pay no extra pipeline parsing.Design: narrow, fail-closed stopgap
This implements the "narrower stopgap" from #6541: exempt only when the whole pipeline is known-inert.
sed/awk(GNU sede, awksystem()//inet/),tee/xargs/find(write or exec),read/yes, and every interpreter or shell are excluded — an unknown command falls through to full validation. This is deliberately more conservative than the "invert detection" approach (only validate URLs that are arguments to network commands), which remains a larger design change for a separate PR.Relationship to #6536
#6536 exempts URLs inside sed/grep/awk pattern arguments (per-URL context). This PR exempts URLs that appear as data in a fully non-network pipeline (per-pipeline context). The two paths are independent, and both are narrowing/guarded. The change here is strictly additive to #6536: it adds new helpers and one insertion point in
process_tool_call(), and does not touch #6536's_is_in_text_pattern_context,_sed_script_writes_or_executes, or_SHELL_REENTRY.Residual (by design)
A
/dev/tcppath assembled purely from shell variables with no literal URL (A=/dev/tc; B=p/H/80; read x < ${A}${B}) is not blocked by this hook — there is no URL to validate, and this is no worse thanmain, which has no inert path or/devguard at all. The sandbox network policy is the enforcing layer; this hook is one defense-in-depth part of it. Thesed-stage repros in #6541 also remain blocked, becausesedis intentionally not inert.Testing
ssrf_pretool_test.pypass (main's 150 pattern-context tests + 73 new inert-pipeline/regression tests), no class-name collisions on merge with fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536's suite (pytest --collect-onlyverified).internal/security/hooks/pass.ruff check,ruff format --check, andty checkpass.Checklist