UNPKG

ai

Version:

AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.

118 lines (89 loc) 5 kB
--- title: Secure URL Fetching description: How the AI SDK protects server-side fetches of URLs returned by model providers, and how to harden your deployment further. --- # Secure URL Fetching Many providers return a **URL in their response body** — a generated image, audio, or video to download, or a polling URL to check job status. The AI SDK fetches these server-side and returns the result to your code. Because that URL comes from an external service, a malicious or compromised provider (or anyone able to tamper with the response) could point it at an internal address such as a cloud-metadata endpoint (`http://169.254.169.254/…`), a private host (`http://10.0.0.5/…`), or `localhost`. To prevent that, the SDK validates every response-supplied URL before fetching it. This happens automatically inside the provider packages — you don't need to configure anything. ## What the SDK protects against When the SDK fetches a URL taken from a provider response, it: - **Rejects private, loopback, and link-local targets**IPv4 (`10/8`, `172.16/12`, `192.168/16`, `127/8`, `169.254/16`, CGNAT, multicast, ) and the equivalent IPv6 ranges, plus `localhost` and `.local`. Non-`http(s)` schemes are rejected too. - **Re-validates every redirect hop** — a URL that passes but then redirects to an internal address is blocked; the redirect is never followed blindly. - **Strips risky request headers** — proxy-forwarding, cloud-metadata, and cookie headers are removed before the request. - **Drops credentials across origins** — caller headers (`Authorization`, `Cookie`, and provider-specific API-key headers alike) are not sent to a host on a different origin than the provider's; a redirect that crosses origin drops all of them except the user-agent. A blocked URL surfaces as a `DownloadError`. ## Self-hosted and local endpoints URLs that are same-origin with the provider endpoint **you configured** (e.g. a custom `baseURL` pointing at a self-hosted or `localhost` deployment) are exempt from these checks — they target exactly the host you told the SDK to talk to. Any redirect off that origin is still validated. ## Limitation: DNS resolution and DNS rebinding The built-in guard inspects the URL **as a string**. It deliberately does **not resolve DNS**, so two attacks remain out of scope at this layer: 1. **Hostname that resolves to a private IP** — a literal host that looks public but whose DNS record points at an internal address. 2. **DNS rebinding** — a host that resolves to a public IP when validated and a private IP a moment later when the socket actually connects (a time-of-check/time-of-use window). ### Why this isn't built in Closing these requires resolving DNS and pinning the resolved IP **at connect time**Node-only capabilities (`node:dns`, a custom `undici` dispatcher). The SDK's provider utilities are **cross-runtime**: they run on the edge, in the browser, and on Bun/Deno, with no Node-only dependencies, so those APIs aren't available there. The threat is also specifically a **server-side** one — on the edge and in the browser, outbound `fetch` cannot reach a host's internal network or metadata endpoint in the first place. So connect-time IP pinning is only meaningful, and only available, on a Node server — which is exactly where you can add it yourself. ## Hardening your deployment If your server fetches provider-supplied URLs and you want to close the DNS gaps, use one (ideally both) of these: ### 1. Restrict outbound egress at the network layer Deny your server's network egress to `169.254.0.0/16`, RFC-1918 ranges, and loopback. This is the most robust control and is independent of application code. ### 2. Inject a hardened `fetch` Every provider accepts a custom `fetch`. On Node, back it with an `undici` `Agent` whose `connect.lookup` validates the resolved IP and lets the socket connect only to a safe address — closing both the hostname-to-private and the DNS-rebinding windows: ```ts import { Agent, fetch as undiciFetch } from 'undici'; import { lookup } from 'node:dns'; // Your own check that returns true for private/loopback/link-local addresses. declare function isUnsafeAddress(ip: string): boolean; const safeLookup: typeof lookup = (hostname, options, callback) => { lookup(hostname, options as any, (err, address, family) => { if (!err && typeof address === 'string' && isUnsafeAddress(address)) { callback(new Error(`Refusing to connect to ${address}`), '', 0); return; } (callback as any)(err, address, family); }); }; const safeDispatcher = new Agent({ connect: { lookup: safeLookup } }); const safeFetch: typeof fetch = (input, init) => undiciFetch(input, { ...init, dispatcher: safeDispatcher }) as any; ``` ```ts import { createFal } from '@ai-sdk/fal'; const fal = createFal({ fetch: safeFetch }); ``` The SDK's built-in validation and your connect-time pinning are complementary — keep both.