Skip to content

Preflight /fetch URLs with the shared SSRF hostname check - #345

Merged
tylerreckart merged 6 commits into
mainfrom
fix/fetch-ssrf-preflight
Sep 21, 2026
Merged

tylerreckart merged 6 commits into
mainfrom
fix/fetch-ssrf-preflight

Conversation

@cursor

@cursor cursor Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

/browse already runs ssrf_preflight_url (metadata-host denylist, literal private ranges, resolve-to-private). cmd_fetch and cmd_fetch_bytes only installed CURLOPT_OPENSOCKETFUNCTION, which sees the TCP peer after curl's resolver — and after an env HTTP(S)_PROXY hop.

That left two gaps vs /browse:

  • metadata hostnames (metadata.google.internal, …) that resolve to a public IP
  • private/metadata URLs fetched through a public proxy (opensocket allows the proxy address)

Agent /fetch, TUI/CLI fetch, and URL-form image parts all share those entry points. The browse comment already claimed /fetch enforced the same blocklist.

Fix

Call the existing preflight_ssrf_check wrapper before curl_easy_init in both fetch paths. Opensocket still re-validates every connect, including after redirects. Error shape matches /browse: refused — <reason> (SSRF guard).

Tests

unit_commands — metadata hostname + 127.0.0.1 / 169.254.169.254 / [::1] literals for cmd_fetch and cmd_fetch_bytes (no DNS). 4 fetch cases locally; ASan/UBSan driver clean. Full unit_commands 66/66.

Independently mergeable against main (0456350). Code hunks (cmd_fetch / cmd_fetch_bytes + fetch-section tests) auto-merge with #328 (cmd_write bak/FIFO, different hunks). CHANGELOG [Unreleased] sibling-conflicts as usual after the first of #321–#344 merges.

Open in Web View Automation 

Note

Low Risk
Narrows SSRF on existing fetch entry points using the same preflight already used by /browse; connect-time guards remain, with new unit tests for blocked URLs.

Overview
/fetch now runs the shared SSRF hostname preflight before libcurl, matching /browse instead of relying only on connect-time CURLOPT_OPENSOCKETFUNCTION.

cmd_fetch and cmd_fetch_bytes call preflight_ssrf_check (wrapper around ssrf_preflight_url) immediately after the http(s) scheme check and before curl_easy_init. That closes gaps where metadata hostnames that resolve to public IPs, or private/metadata targets sent via HTTP(S)_PROXY, could slip past opensocket validation. Failures use the same shape as browse: refused — <reason> (SSRF guard) (or ERR: for cmd_fetch). Opensocket still re-validates every connect, including redirects; /browse comments were updated to note parity.

Tests add unit_commands coverage for denylisted metadata hostnames and literal private ranges on both fetch entry points (no DNS). CHANGELOG records the fix under [Unreleased] → Fixed.

Reviewed by Cursor Bugbot for commit 1a6c6d2. Bugbot is set up for automated code reviews on this repo. Configure here.

cmd_fetch and cmd_fetch_bytes only blocked private peers at connect
time. Metadata hostnames that resolve public, and private/metadata
targets reached through HTTP_PROXY (opensocket sees the proxy),
skipped the denylist /browse already runs. Call ssrf_preflight_url
before libcurl; opensocket still re-checks every redirect connect.

Co-authored-by: Tyler Reckart <tylerreckart@users.noreply.github.com>
@tylerreckart
tylerreckart marked this pull request as ready for review September 21, 2026 12:59
@tylerreckart
tylerreckart enabled auto-merge (squash) September 21, 2026 12:59
@tylerreckart
tylerreckart merged commit 7566810 into main Sep 21, 2026
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants