Skip to content

fix(#6541): skip SSRF validation for inert pipelines - #6542

Open
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6541-ssrf-inert-pipeline-exempt
Open

fix(#6541): skip SSRF validation for inert pipelines#6542
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6541-ssrf-inert-pipeline-exempt

Conversation

@fullsend-ai-coder

@fullsend-ai-coder fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Skip SSRF URL validation in ssrf_pretool.py when the entire Bash command pipeline consists of known-inert (non-network) commands. This fixes false-positive blocks on commands like echo URL | cut -d/ -f4,5 where 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

  • Add _INERT_COMMANDS frozenset (~45 commands that cannot make network requests: echo, printf, cat, grep, cut, tr, jq, sort, base64, sha256sum, …).
  • Add _pipeline_is_inert(): fail-closed check that returns True only when every pipeline stage is inert and there is no substitution, shell reentry, /dev/tcp-style device access, or variable-assembled redirection target.
  • Add _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.
  • Add _split_command_stages(): quote-aware splitter for |, ;, &&, ||, &, and newlines.
  • Add _extract_base_command(): extracts the command name from a stage, skipping variable assignments and path prefixes.
  • Add _has_dev_net_device() + _strip_shell_quoting(): detect /dev/tcp and /dev/udp including 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 in process_tool_call(), before the inert bypass.
  • In 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 sed e, awk system()//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/tcp path 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 than main, which has no inert path or /dev guard at all. The sandbox network policy is the enforcing layer; this hook is one defense-in-depth part of it. The sed-stage repros in #6541 also remain blocked, because sed is intentionally not inert.

Testing

  • 223 tests in ssrf_pretool_test.py pass (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-only verified).
  • All 472 hook tests in internal/security/hooks/ pass.
  • ruff check, ruff format --check, and ty check pass.

Checklist

  • PR title follows Conventional Commits
  • Commit is signed off (DCO)
  • I can explain all changes in this contribution

@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from a team as a code owner August 23, 2026 20:54
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Agent PR ready for human review label Aug 23, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:56 PM UTC · Completed 9:14 PM UTC

Commit: 2050667 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.79

@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

Review

Findings

Low

  • [false-positive] internal/security/hooks/ssrf_pretool.py_has_dev_net_device checks the raw command string after _strip_shell_quoting without quoting context, so /dev/tcp paths inside string literals (e.g., echo '/dev/tcp/host/port') are flagged even though the path is data, not a redirection target. Conservative (over-blocking), not a security gap.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_split_command_stages splits on unquoted newlines, so here-document body lines are treated as separate pipeline stages. _pipeline_is_inert returns False for commands containing heredocs (conservative false positive). Not a security gap — commands fall through to full URL validation.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_REDIRECT_VAR_PATTERN is applied after _strip_shell_quoting removes all quoting, so patterns like echo '> $var' (where > $var is inside single quotes and is data) cause _pipeline_is_inert to return False. Conservative false positive, not a bypass.

Previous run

Review

Findings

High

  • [logic-error] internal/security/hooks/ssrf_pretool.py_has_dev_net_device strips single and double quotes but does not strip backslash escapes. In Bash, unquoted backslash-letter sequences undergo quote removal: /dev/\tcp/HOST/80 becomes /dev/tcp/HOST/80 at runtime. For example, read line < /dev/\tcp/metadata.google.internal/80: (1) _has_dev_net_device sees /dev/\tcp/ after quote stripping — regex does not match \tcp; (2) _pipeline_is_inert returns True because read is in _INERT_COMMANDS; (3) URL validation is skipped entirely; (4) at runtime, Bash removes the backslash and opens a TCP connection via the /dev/tcp virtual device.
    Remediation: In _has_dev_net_device, also strip backslash characters before matching _DEV_NET_PATTERN — e.g., stripped = stripped.replace('\\', '') or re.sub(r'\\(.)', r'\\1', stripped).

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.py_pipeline_is_inert() combined with _has_dev_net_device() can be bypassed via split-variable construction of /dev/tcp paths. export A=/dev/tc; export B=p/169.254.169.254/80; read line < ${A}${B} — all three base commands (export, export, read) are in _INERT_COMMANDS, _has_command_substitution does not flag ${VAR} (only $(cmd) and backticks), _has_dev_net_device sees neither /dev/tcp/ nor /dev/$ in the command, and URL validation is skipped entirely. At runtime Bash resolves the variables and read opens a TCP connection to the metadata endpoint.
    Remediation: Reject inert classification when both /dev/ and a $ character appear anywhere in the command (conservative), or remove read from _INERT_COMMANDS since it is the only listed builtin that can open /dev/tcp connections via Bash redirection.

Medium

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.py_INERT_COMMANDS includes read, which is the only Bash builtin in the set capable of opening /dev/tcp and /dev/udp network connections via input redirection. While _has_dev_net_device provides a guard, that guard is bypassable (see high-severity findings). Including read in the inert list widens the attack surface. See also: [logic-error] and [SSRF-bypass] high-severity findings.
    Remediation: Remove read from _INERT_COMMANDS. Commands using read will fall through to URL validation, which is the safer default.

  • [logic-error] internal/security/hooks/ssrf_pretool.py_has_dev_net_device can be bypassed using ANSI-C quoting ($'...') for individual characters. /dev/tc$'p'/HOST/80: (1) quote stripping removes the single quotes yielding /dev/tc$p/HOST/80 — regex does not match; (2) _DEV_VAR_PATTERN looks for /dev/$ but the $ is at position /dev/tc$ not /dev/$; (3) _has_command_substitution does not flag $' (correct shell behavior — ANSI-C quoting is not command substitution); (4) at runtime Bash resolves $'p' to literal p, producing /dev/tcp/HOST/80.
    Remediation: After stripping quotes and backslashes, also normalize ANSI-C quoting fragments — the simplest approach is to strip all ', ", and \ characters before regex matching.

Low

  • [false-positive] internal/security/hooks/ssrf_pretool.py — The /dev/tcp and /dev/udp check runs against the raw command string with no quoting context. Commands like echo '/dev/tcp/host/port' or grep '/dev/tcp' file are blocked even though the string is data, not a redirection target. Conservative (over-blocking), not a security gap.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_split_command_stages splits on unquoted newlines, so here-document body lines are treated as separate pipeline stages. This causes _pipeline_is_inert to return False (conservative false positive). Not a security gap — commands fall through to full URL validation.

  • [scope-boundary] internal/security/hooks/ssrf_pretool.py — Both this PR and PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536 modify ssrf_pretool.py and are open against main. The PRs touch different sections of process_tool_call() and the PR body documents their complementary nature. Integration testing after both merge would be prudent.

  • [authorization-tier] internal/security/hooks/ssrf_pretool.py — Issue ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541 carries both needs-design and ready-to-code labels. The PR implements the conservative "narrower stopgap" approach described in the issue. A human maintainer confirming this design choice would resolve the label ambiguity.

  • [defense-in-depth] internal/security/hooks/ssrf_pretool.py_INERT_COMMANDS includes yes which can generate unbounded output. While not a network-access concern, pairing yes with a redirect could fill disk. Outside the SSRF dimension — informational only.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Medium

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.py_DEV_NET_PATTERN can be evaded via two vectors: (a) Partial quoting — the regex /dev/['"]?(?:tcp|udp)['"]?/ only handles quotes wrapping the entire tcp/udp token. Bash performs quote removal character-by-character, so /dev/tc'p'/HOST/80 resolves to /dev/tcp/HOST/80 at runtime but does not match the regex. When the base command is in _INERT_COMMANDS (e.g., read line < /dev/tc'p'/169.254.169.254/80), _pipeline_is_inert returns True, bypassing URL validation entirely. (b) Variable expansion — export PROTO=tcp; read line < /dev/$PROTO/169.254.169.254/80 bypasses the regex because /dev/$PROTO/ does not match. _has_command_substitution only detects $() (command substitution), not $PROTO (variable expansion). Both export and read are in _INERT_COMMANDS, so the pipeline is classified as inert.
    Remediation: Strip all single and double quotes from substrings following /dev/ before matching, or use a broader regex. Additionally scan for /dev/ followed by a variable expansion pattern ($).

  • [logic-error] internal/security/hooks/ssrf_pretool.py_has_command_substitution does not track double-quote context, while _split_command_stages does. A single quote inside double quotes (e.g., "'") causes _has_command_substitution to incorrectly enter single-quote mode, hiding subsequent command substitutions. For example, echo "'"$(curl http://metadata-endpoint/latest/)"'" contains a live command substitution, but _has_command_substitution returns False because it thinks $(curl ...) is inside single quotes. Combined with echo being in _INERT_COMMANDS, _pipeline_is_inert returns True and URL validation is skipped.
    Remediation: Add double-quote tracking to _has_command_substitution, mirroring the approach in _split_command_stages. When in_dq is True, single quotes should not toggle in_sq.

Low

  • [false-positive] internal/security/hooks/ssrf_pretool.py — The /dev/tcp and /dev/udp check runs against the raw command string with no quoting context. Commands like echo '/dev/tcp/host/port' or grep '/dev/tcp' file are blocked even though the string is data, not a redirection target. Conservative (over-blocking), not a security gap.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_split_command_stages splits on unquoted newlines, so here-document body lines are treated as separate pipeline stages. This causes _pipeline_is_inert to return False (conservative false positive). Not a security gap — commands fall through to full URL validation.

  • [scope-boundary] internal/security/hooks/ssrf_pretool.py — Both this PR and PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536 modify ssrf_pretool.py and are open against main. The PRs touch different sections of process_tool_call() and the PR body documents their complementary nature. Integration testing after both merge would be prudent.

  • [authorization-tier] internal/security/hooks/ssrf_pretool.py — Issue ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541 carries both needs-design and ready-to-code labels. The PR implements the conservative "narrower stopgap" approach described in the issue. A human maintainer confirming this design choice would resolve the label ambiguity.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

Medium

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.py_DEV_NET_PATTERN uses a literal regex /dev/(?:tcp|udp)/ that can be evaded by inserting shell quotes into the path. Bash processes /dev/tcp as a virtual network device during redirection handling, which occurs after quote removal, so read line < /dev/'tcp'/169.254.169.254/80 is equivalent to the unquoted form at runtime but does not match the regex. When the base command is in _INERT_COMMANDS (e.g., read), _pipeline_is_inert returns True, bypassing URL validation entirely. Since /dev/tcp paths don't use URL syntax, URL_PATTERN would not catch them either. This is a gap in a new defense — the base branch has no /dev/tcp protection — but the code comment claims to "block unconditionally."
    Remediation: Normalize the command string by stripping shell quotes from /dev/ paths before matching, or use a quote-aware regex like /dev/['"]?(?:tcp|udp)['"]?/.

  • [code-organization] internal/security/hooks/ssrf_pretool_test.py — The test file has no subprocess-based integration tests that exercise the hook as a standalone process. All other test files in internal/security/hooks/ use a _run_hook() subprocess pattern to verify the stdin/stdout/exit-code protocol contract end-to-end. This file tests only via in-process importlib loading.
    Remediation: Add subprocess-based tests for at least one allowed (inert pipeline) and one blocked case, verifying exit code and JSON output.

Low

  • [edge-case] internal/security/hooks/ssrf_pretool.py_SHELL_REENTRY regex requires -c to immediately follow the shell name with only whitespace. It misses combined flags like bash -lc, bash -ic, sh -xc, or intervening flags like bash --norc -c. Currently mitigated because bash/sh/etc. are not in _INERT_COMMANDS, so _pipeline_is_inert returns False for any stage whose base command is a shell. Defense-in-depth improvement.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_extract_base_command can return an empty string instead of None when rsplit("/", 1)[-1] yields "". Safe in practice because "" is not in _INERT_COMMANDS, but violates the documented contract that the function returns None when the command cannot be determined.

  • [false-positive] internal/security/hooks/ssrf_pretool.py — The /dev/tcp and /dev/udp check runs against the raw command string with no quoting context. Commands like echo '/dev/tcp/host/port' or grep '/dev/tcp' file are blocked even though the string is data, not a redirection target. Conservative (over-blocking), not a security gap.

  • [edge-case] internal/security/hooks/ssrf_pretool.py_split_command_stages splits on unquoted newlines, so here-document body lines are treated as separate pipeline stages. This causes _pipeline_is_inert to return False (conservative false positive). Not a security gap — commands fall through to full URL validation.

  • [scope-boundary] internal/security/hooks/ssrf_pretool.py — Both this PR and PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536 modify ssrf_pretool.py and are open against main. The PRs touch different sections of process_tool_call() and the PR body documents their complementary nature. Integration testing after both merge would be prudent.

  • [authorization-tier] internal/security/hooks/ssrf_pretool.py — Issue ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541 carries both needs-design and ready-to-code labels. The PR implements the conservative "narrower stopgap" approach described in the issue. A human maintainer confirming this design choice would resolve the label ambiguity.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

Critical

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.pyawk, gawk, mawk, and nawk are classified as inert commands but all support arbitrary command execution via system(), getline-from-pipe, and print-to-pipe built-in functions. An attacker-influenced prompt can craft commands like awk 'BEGIN{system("curl http://metadata-endpoint/")}' that bypass the SSRF hook entirely because _pipeline_is_inert returns True. gawk additionally has native TCP/UDP networking via /inet/ special files, making it a direct SSRF vector without needing an external binary.
    Remediation: Remove awk, gawk, mawk, and nawk from _INERT_COMMANDS.

High

  • [SSRF-bypass] internal/security/hooks/ssrf_pretool.pysed is classified as an inert command but GNU sed supports the e command and e flag on substitution commands, which execute the pattern space as a shell command. An attacker-influenced prompt can craft echo x | sed 's/.*/curl http://metadata-endpoint/e' to execute arbitrary network requests. GNU sed is the default on Linux.
    Remediation: Remove sed from _INERT_COMMANDS.

Medium

Low

  • [file-write-bypass] internal/security/hooks/ssrf_pretool.pytee is classified as inert but writes stdin to arbitrary files. While not directly SSRF, it could enable privilege escalation if the sandbox filesystem is insufficiently locked down.

  • [scope-boundary] internal/security/hooks/ssrf_pretool.py — Both this PR and PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536 modify ssrf_pretool.py and are open against main. The PR body addresses their interaction, but the coordination risk deserves attention.

  • [code-organization] internal/security/hooks/ssrf_pretool_test.py:29 — The test file uses a pytest fixture (hook) to load the module, while no other test file in this directory uses pytest fixtures.


Labels: PR modifies SSRF security hook in internal/security/hooks/


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added security Security threat model and related concerns component/sandbox OpenShell sandbox environment labels Aug 23, 2026
@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 9:16 PM UTC · Completed 9:24 PM UTC

Commit: 2050667 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.55

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 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):

  1. awk/gawk/mawk/nawk have system(), getline-from-pipe, and gawk has native TCP/UDP networking (internal/security/hooks/ssrf_pretool.py): Removed awk, gawk, mawk, nawk from _INERT_COMMANDS. Added unit tests confirming all four are non-inert and an integration test for awk system() bypass.
  2. sed has GNU 'e' command/flag for shell execution (internal/security/hooks/ssrf_pretool.py): Removed sed from _INERT_COMMANDS. Added unit tests confirming sed pipelines are non-inert and an integration test for sed 'e' flag bypass. Removed sed-based false-positive integration tests (those false positives are delegated to PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536's per-argument context approach).
  3. /dev/tcp and /dev/udp Bash virtual devices bypass SSRF check (internal/security/hooks/ssrf_pretool.py): Added _DEV_NET_PATTERN regex and a check in process_tool_call that blocks /dev/tcp and /dev/udp redirections unconditionally, before the inert-pipeline check. Added three integration tests covering /dev/tcp, /dev/udp, and /dev/tcp in an otherwise-inert pipeline.
  4. tee writes to arbitrary files — not safe for inert exemption (internal/security/hooks/ssrf_pretool.py): Removed tee from _INERT_COMMANDS. Added unit test confirming tee is non-inert and an integration test confirming tee pipelines go through URL validation.

Disagreed (3):

  1. authorization-tier: needs-design label on ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541 suggests design review was expected: This is a process concern about issue labeling, not a code defect. Resolving the needs-design label is a human maintainer decision outside the scope of the fix agent.
  2. scope-boundary: coordination risk with PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536: This is an awareness item about concurrent PRs. The PR body already documents the relationship. No code change is needed.
  3. code-organization: test file uses pytest fixture while other test files do not: Changing the test fixture pattern is a style refactor unrelated to the security findings. Out of scope for this fix.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

fullsend-ai-coder Bot added a commit that referenced this pull request Aug 23, 2026
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
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:26 PM UTC · Completed 9:45 PM UTC

Commit: ba7fbf5 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.83

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 9:47 PM UTC · Completed 9:55 PM UTC

Commit: ba7fbf5 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.42

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 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):

  1. _DEV_NET_PATTERN shell-quote evasion bypass (internal/security/hooks/ssrf_pretool.py): Made _DEV_NET_PATTERN regex quote-aware to catch /dev/'tcp'/HOST/80 and /dev/"tcp"/HOST/80 evasion. Bash strips quotes before processing virtual device paths, so quoted variants are equivalent at runtime. Added 3 test cases covering single-quoted tcp, double-quoted tcp, and single-quoted udp evasion.
  2. Test file lacks subprocess-based integration tests (internal/security/hooks/ssrf_pretool_test.py): Added TestSubprocessProtocol class with _run_hook() subprocess helper matching the convention used by other hook test files (canary_pretool_test.py, etc.). Covers: inert pipeline allowed (exit 0, no stdout), curl to metadata blocked (exit 1, block JSON), /dev/tcp blocked, empty input allowed, and malformed JSON fail-closed.
  3. _SHELL_REENTRY misses combined flags like bash -lc, sh -xc (internal/security/hooks/ssrf_pretool.py): Broadened _SHELL_REENTRY regex to match combined flags (-lc, -xc, -ic) and intervening flags (--norc -c). Defense-in-depth: currently mitigated because shell names are not in _INERT_COMMANDS, but the regex now correctly detects shell reentry regardless. Added 4 test cases.
  4. _extract_base_command returns empty string instead of None for trailing slash (internal/security/hooks/ssrf_pretool.py): Added 'or None' guard so rsplit('/', 1)[-1] returning '' is converted to None, matching the documented contract. Safe in practice ('' not in _INERT_COMMANDS) but now correct per the function's docstring. Added test case.

Disagreed (4):

  1. /dev/tcp check false-positives on data strings like echo '/dev/tcp/host/port': Reviewer explicitly notes this is 'conservative (over-blocking), not a security gap.' Over-blocking is the correct tradeoff for a security-sensitive SSRF hook. Making the check quote-context-aware would add significant complexity for minimal benefit and could introduce new bypass vectors.
  2. _split_command_stages treats here-doc body lines as separate stages: Reviewer explicitly notes this is 'not a security gap — commands fall through to full URL validation.' The conservative behavior (false positive) is the safe default. Implementing here-doc awareness would add substantial parser complexity with no security benefit.
  3. Both this PR and PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536 modify ssrf_pretool.py (scope-boundary): Coordination note, not a code change request. The PR body already documents the complementary relationship. Integration testing after both merge is a post-merge concern.
  4. Issue ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541 label ambiguity between needs-design and ready-to-code: Process concern for human maintainers, not actionable by the fix agent. The PR implements the conservative 'narrower stopgap' approach explicitly described in the issue.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

fullsend-ai-coder Bot added a commit that referenced this pull request Aug 23, 2026
- 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
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:57 PM UTC · Completed 10:16 PM UTC

Commit: f82ba4e · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.41

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 10:18 PM UTC · Completed 10:26 PM UTC

Commit: f82ba4e · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $2.52

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 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):

  1. SSRF-bypass: _DEV_NET_PATTERN evasion via partial quoting and variable expansion (internal/security/hooks/ssrf_pretool.py): Replaced _DEV_NET_PATTERN regex with _has_dev_net_device() function that strips all single/double quotes before matching (closing partial-quoting evasion like /dev/tc'p'/HOST/80) and additionally checks for /dev/$ variable expansion patterns (closing /dev/$PROTO/HOST/80 evasion). Added 4 integration tests for partial quoting, variable expansion, and curly-brace variable expansion.
  2. logic-error: _has_command_substitution missing double-quote context tracking (internal/security/hooks/ssrf_pretool.py): Added in_dq (double-quote) state tracking to _has_command_substitution(), mirroring the approach in _split_command_stages(). When in_dq is True, single quotes no longer toggle in_sq (they are literal inside double quotes), and $() / backticks are still detected (active inside double quotes), while <() / >() are correctly treated as literal. This prevents single quotes inside double quotes from hiding command substitutions. Added 8 tests covering double-quote context behavior at unit and integration levels.

Disagreed (4):

  1. false-positive: /dev/tcp check blocks data usage in echo/grep: Reviewer explicitly notes this is conservative over-blocking, not a security gap. The fail-closed behavior is intentional for a security hook.
  2. edge-case: heredoc body lines treated as separate pipeline stages: Reviewer explicitly notes this causes a conservative false positive (falls through to full URL validation), not a security gap.
  3. scope-boundary: PR coordination with PR fix(#6535): skip SSRF validation for URLs in sed/grep/awk patterns #6536: Informational finding. Both PRs touch different sections of process_tool_call() and the PR body documents their complementary nature. No code change needed.
  4. authorization-tier: needs-design and ready-to-code label ambiguity on issue ssrf_pretool.py still blocks URL literals passed as data to non-network commands (echo/here-string/assignment) #6541: Requires human maintainer action to resolve label ambiguity. Out of scope for automated fix — this is a project governance decision.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

fullsend-ai-coder Bot added a commit that referenced this pull request Aug 23, 2026
…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
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:28 PM UTC · Completed 10:47 PM UTC

Commit: 675510e · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.64

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ❌ Failure · Started 10:49 PM UTC · Completed 11:01 PM UTC

Commit: 675510e · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.81

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

⚠️ Post-fix script failed — Signed-off-by rejected (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32671662930

Details:
Agent commit contains a Signed-off-by trailer. Agents must not use 'git commit -s' or append Signed-off-by trailers.
Please check the workflow logs for full details and retry with /fs-fix if appropriate.

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>
@waynesun09
waynesun09 force-pushed the agent/6541-ssrf-inert-pipeline-exempt branch from 675510e to f6843d5 Compare August 24, 2026 12:15
@waynesun09

Copy link
Copy Markdown
Member

Thanks — all 14 findings addressed in the rebased commit (f6843d50, force-pushed onto current main). Disposition:

Fixed by removing from _INERT_COMMANDS (fail closed to full validation):

  • [critical] awk/gawk/mawk/nawk — not inert (system(), getline-from-pipe, gawk /inet/).
  • [high] sed — not inert (GNU e command/flag executes a shell).
  • [low] tee — not inert (writes arbitrary files).
  • [medium] read — removed; the only builtin that opens /dev/tcp via <.
  • [low] yes — removed (unbounded output).

Fixed in /dev/tcp handling (_has_dev_net_device + _strip_shell_quoting, blocked outright before the inert bypass):

  • [medium] /dev/tcp bypass via echo/read — now returns a block unconditionally.
  • [medium] partial-quote / variable evasion (/dev/tc'p'/, /dev/$PROTO/) — quotes stripped; _DEV_VAR_PATTERN broadened to /dev/\S*\$.
  • [high] backslash /dev/\tcp/_strip_shell_quoting now also strips backslashes.
  • [medium] ANSI-C /dev/tc$'p'/ (incl. $'\x70' hex) — the surviving $ is caught by the broadened /dev/\S*\$.

Fixed in substitution detection:

  • [medium] _has_command_substitution double-quote context — now tracks double quotes, so an apostrophe inside "…" cannot hide a later $()/backtick.

Fixed as convention:

Accepted as conservative over-blocking (not security gaps), documented in the PR body's Residual section:

  • [low] /dev/ check has no quoting contextecho '/dev/tcp/…' as data is blocked. Over-block, acceptable.
  • [low] newline splits heredoc bodies — pushes such pipelines to full validation (fail closed).
  • [high] split-variable /dev/tcp (${A}${B}) — flagged non-inert via _REDIRECT_VAR_PATTERN; with no literal URL there is nothing to validate, so it is allowed here exactly as on main. The sandbox network policy is the enforcing layer; this hook is defense-in-depth.
  • [low] yes flooding — moot; yes was removed.

Verified: 223 tests in ssrf_pretool_test.py, 472 across internal/security/hooks/, ruff + ty clean.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 24, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:17 PM UTC · Completed 12:59 PM UTC

Commit: f6843d5 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself August 24, 2026 12:59

Superseded by updated review

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/sandbox OpenShell sandbox environment ready-for-merge All reviewers approved — ready to merge ready-for-review Agent PR ready for human review security Security threat model and related concerns

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants