Skip to content

fix(provider-utils): route provider-response URLs through the download-URL guard#16971

Open
dnukumamras wants to merge 1 commit into
mainfrom
dnukumamras/getfromapi-url-validation
Open

fix(provider-utils): route provider-response URLs through the download-URL guard#16971
dnukumamras wants to merge 1 commit into
mainfrom
dnukumamras/getfromapi-url-validation

Conversation

@dnukumamras

@dnukumamras dnukumamras commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What this does

getFromApi in @ai-sdk/provider-utils fetches URLs that come from provider response bodies (image, audio, and video downloads, and polling URLs). Those URLs are untrusted, but they were fetched with no scheme, host, or IP checks. This PR routes them through the same guard that downloadBlob already uses.

  • Added an opt-in validateUrl flag to getFromApi that routes the request through fetchWithValidatedRedirects, which rejects private, loopback, and link-local targets and re-validates every redirect hop. Blocked URLs throw DownloadError.
  • Added header sanitization on the validated path: proxy, forwarding, cloud-metadata, and cookie headers are stripped before the request.
  • Drop Authorization and Cookie on any redirect that crosses origin, so credentials never follow a redirect to a different host.
  • Added a credentialedOrigin option so caller headers are sent only when the URL is same-origin with the provider's own origin, and withheld otherwise.
  • Enabled the guard at the 11 call sites that fetch a URL from a response body, across fal, luma, gladia, fireworks, xai, black-forest-labs, and replicate.
  • Closed two range gaps in validateDownloadUrl: IPv4 224.0.0.0/4 (multicast) and IPv6 2001:db8::/32 (documentation).

Secure by default going forward

  • validateUrl is a required field, with no default. Every current and future getFromApi call site has to make an explicit choice, so a new integration cannot silently inherit an unsafe default.
  • true for a URL that comes from a response body, false for a URL built from a configured baseURL. The compiler will not let a new call site skip the decision.
  • credentialedOrigin folds credential gating into the same call, so a new integration that opts into validation gets first-hop credential protection without hand-rolling a same-origin check.
  • Documented the rule in AGENTS.md, contributing/add-new-provider.md, and contributing/secure-url-handling.md, plus a user-facing page in content/docs.

Happy path

  • A normal public asset or polling URL passes validation and is fetched and returned exactly as before, same return type, error handling, abort signal, and user-agent suffix.
  • Same-origin polling URLs still receive the provider API key.
  • Redirects to other public hosts are still followed, one hop at a time, each re-validated.

Unhappy path

  • A response URL pointing at a private, loopback, or link-local address (for example a cloud-metadata or internal host) is rejected with DownloadError and never fetched.
  • A URL that starts public but redirects to an internal address is blocked at the redirect hop.
  • Credentials are withheld when the URL is not same-origin with credentialedOrigin, and dropped if a redirect crosses origin.

Not solved here

The guard does string and literal-IP checks and does not resolve DNS, so a hostname that resolves to a private address, and DNS rebinding, are out of scope at this layer. These need Node-only APIs (undici, node:dns) that are not available across all runtimes this package targets (edge, browser, Bun), and the threat is server-only. content/docs/06-advanced/11-secure-url-fetching.mdx explains how a server deployment closes the gap: restrict network egress, or inject a Node fetch that pins the resolved IP at connect time.

…d-URL guard

Make getFromApi validate URLs taken from provider responses and require an
explicit trust decision at every call site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant