Skip to content

Shared outbound fetch helper with connect-time SSRF checks - #821

Merged
keysersoft merged 2 commits into
mainfrom
keysersoft/outbound-fetch-helper
Oct 2, 2026
Merged

keysersoft merged 2 commits into
mainfrom
keysersoft/outbound-fetch-helper

Conversation

@keysersoft

Copy link
Copy Markdown
Contributor

Adds fetchOutbound() in packages/backend/src/common/outbound-fetch.util.ts, the shared helper promised in #645 for downloading a URL that comes from a user or a model (first user: the multipart file parts in #818).

What it does:

  • Runs the SSRF guard on the URL and again on every redirect target (redirects are followed by hand, max 5 by default).
  • Connects through http/https agents whose lookup applies the guard at connect time, so the socket only reaches addresses the guard checked. These are exported from ssrf.util as ssrfGuardedLookup() and createSsrfGuardedAgents().
  • Sends only the caller's headers, ignores env proxies, and drops Authorization/Cookie if a redirect goes to another origin.
  • One deadline for the whole exchange (30 s default) and a byte cap (10 MB default) checked on Content-Length and on the bytes actually read. The body comes back as a Buffer, so it can be resent on a 401 retry.
  • Non-2xx is an OutboundFetchError with reason: 'status'. Messages use redactUrl() (origin + path, no query string, no userinfo).
  • A blocked hop still throws SsrfBlockedError, so the existing allowlist hint keeps working.

assertSafeOutboundHost() is refactored onto the same internal check and keeps its behaviour and error messages.

Tests: 16 new cases with local servers (redirect to an internal address, DNS answer that changes between check and connect, non-http redirect, redirect loop, cross-origin header drop, non-2xx, Content-Length and streamed cap, deadline, env proxy). Removing the guarded agents, the per-hop check or proxy: false each makes at least one of them fail. Full backend suite passes locally.

fetchOutbound() downloads a third-party URL for a user: every hop
(the URL and each redirect target) passes the SSRF guard, the socket
connects only to addresses the guard checked, no env proxy, only the
caller's headers (Authorization/Cookie dropped on a cross-origin
redirect), one deadline for the whole exchange, a byte cap on
Content-Length and on the bytes read, non-2xx as errors, and URLs
without query strings in messages.

ssrf.util gains ssrfGuardedLookup() and createSsrfGuardedAgents() so
other callers can close the same gaps; assertSafeOutboundHost keeps
its behaviour and messages.

Needed by #645 (multipart file parts fetched from a URL).
@keysersoft
keysersoft merged commit 7ff82d6 into main Oct 2, 2026
13 checks passed
@keysersoft
keysersoft deleted the keysersoft/outbound-fetch-helper branch October 2, 2026 16:45
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant