Skip to content

fix: validate every redirect hop before fetching InputFile URLs - #124

Merged
abnegate merged 3 commits into
mainfrom
cursor/fix-inputfile-redirect-ssrf-26a7
Oct 15, 2026
Merged

abnegate merged 3 commits into
mainfrom
cursor/fix-inputfile-redirect-ssrf-26a7

Conversation

@abnegate

@abnegate abnegate commented Oct 5, 2026

Copy link
Copy Markdown
Member

Problem

Hosted InputFile URL fetches (_fetch_input_file) validated the caller-supplied URL, then let httpx follow redirects automatically and only re-checked resp.url after the final request had already been sent. A public host that answered 302 Location: http://169.254.169.254/... (or loopback / RFC1918) made the server issue a GET to that internal address. The post-fetch check discarded the body, but the request had already happened.

Fetch failures were also an oracle: a blocked internal hop, an HTTP error, and a connection failure each produced different caller-visible text, and httpx errors included the final URL and status code.

Fix

  • Disable automatic redirects and follow Location manually up to the existing FETCH_MAX_REDIRECTS cap.
  • Resolve and validate every hop (initial URL and each Location, including relative ones via urljoin) before opening a connection to that hop.
  • Pin TCP to the IP just validated and keep the original hostname as Host and TLS sni_hostname, so HTTPS certificate verification is unchanged and the resolve-then-reconnect DNS-rebinding window is closed.
  • Preserve the existing size cap, timeout, redirect cap, and filename derivation for legitimate public URLs.
  • Map redirect / HTTP / connection failures to a single generic caller-visible message. Specific validation errors remain only for the caller-supplied URL (scheme, DNS, private). Details stay in server logs.

Tests

Unit coverage in tests/unit/test_server.py:

  • Public → loopback / private / link-local redirect is refused and a local internal listener receives no request
  • Direct public URL and public → public redirect still work (mocked client and local HTTP servers)
  • Relative Location is joined against the logical hostname, not the pinned IP
  • Redirect cap still enforced
  • HTTP errors, connection errors, and blocked redirect hops all return the same generic message
  • DNS / getaddrinfo is mocked; tests do not use the public internet

Checks

ruff, black --check, pyright, and python -m unittest discover -s tests/unit pass locally.

Open in Web Open in Cursor 

cursoragent and others added 2 commits October 5, 2026 20:23
Follow InputFile URL redirects manually and resolve/validate each hop
before connecting so a public host cannot bounce the hosted server onto
loopback, private, or link-local addresses. Pin TCP to the validated IP
while keeping Host and TLS SNI on the original hostname. Return a
generic fetch error to callers so redirect failures are not an SSRF
oracle.

Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
# Conflicts:
#	tests/unit/test_server.py
@abnegate
abnegate marked this pull request as ready for review October 15, 2026 03:50
@hansi-codes

hansi-codes Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🟢 Tier S · Ready to merge

This change replaces automatic redirects in InputFile URL downloads with manually validated, IP-pinned hops and generic fetch-failure messages. It also adds unit coverage for redirect safety, successful downloads, redirect limits, and failure-message consistency.

Latest changes: The newest commits reuse the webhook egress policy for IP routability checks and add coverage for carrier-grade NAT and NAT64 addresses.

Verdict New comments Fixed Still open
💬 Commented 0 0 0
📂 Walkthrough · 2
File Change
src/mcp_server_appwrite/server.py Adds IP-pinned fetch targets, validates each redirect before connecting, and normalizes fetch failures.
tests/unit/test_server.py Covers redirect validation, pinned requests, successful public redirects, and generic errors.

Reviewed the commits since 7558ab0 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

hansi-codes[bot]
hansi-codes Bot previously requested changes Oct 15, 2026

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread src/mcp_server_appwrite/server.py Outdated
Shared address space (100.64.0.0/10) is neither private nor reserved in
ipaddress, so the fetch guard let redirects reach CGNAT-routed internal
hosts. Reuse the webhook egress guard, which requires is_global and
judges IPv4-mapped and NAT64 forms by their embedded address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@hansi-codes
hansi-codes Bot dismissed their stale review October 15, 2026 04:03

The findings that requested changes are resolved. See the summary comment for what is still open.

@abnegate
abnegate merged commit df8271f into main Oct 15, 2026
6 checks passed
@abnegate
abnegate deleted the cursor/fix-inputfile-redirect-ssrf-26a7 branch October 15, 2026 04:11
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.

2 participants