/** * @license * Copyright 2025 Steven Roussey * SPDX-License-Identifier: Apache-2.0 * * SSRF-aware fetch wrapper. * * The browser default performs a static URL classification only and delegates * to `globalThis.fetch`. The Node/Bun entrypoints register a server-side * implementation (see `SafeFetch.server.ts`) that additionally resolves DNS, * classifies every resolved address, and pins the connection to a specific * IP via an undici Agent — this closes the DNS-rebinding gap. * * Callers pass `allowPrivate` to opt into private/loopback targets. The task * layer sets this flag based on whether the task has been granted the * `network:private` entitlement (via its dynamic `entitlements()`). */ export interface SafeFetchOptions extends RequestInit { /** * When true, requests to private/loopback/link-local/metadata hosts are * permitted. When false (default), such requests throw PermanentJobError * both at URL-classification time and (in the server impl) at DNS-resolution * time — defeating DNS rebinding. */ readonly allowPrivate?: boolean; /** * When `allowPrivate` is true, additionally restrict private-host requests * (including redirect targets) to URLs matching at least one of these glob * patterns. Must match the patterns the task declared in its * `network:private` entitlement scope (see `urlResourcePattern`). When * `undefined`, no scope check is applied and the boolean `allowPrivate` * governs alone — legacy behavior for direct callers outside the task * layer. * * This closes the "compromised redirect escapes grant scope" SSRF gap: * the task-graph enforcer approves a narrow private-host scope at * preflight, and this field re-enforces that same scope on every redirect * hop so a Location header cannot pivot to a different private host or * port the task was never authorized to reach. */ readonly privateResourceScopes?: readonly string[]; /** * Extra request-header names that carry a secret and must be dropped when a * redirect crosses to a different origin. {@link CROSS_ORIGIN_STRIPPED_HEADERS} * is always dropped regardless of this field; this list exists for schemes * that place a credential on a caller-named header (e.g. `X-Api-Key`), which * safeFetch cannot recognize on its own. Matched case-insensitively. */ readonly sensitiveHeaders?: readonly string[]; } /** * Request headers always dropped on a cross-origin redirect hop. Following a * redirect must not replay ambient authority to a host the caller never chose: * the redirect target is named by the previous server, which may itself be * compromised or attacker-influenced. */ export declare const CROSS_ORIGIN_STRIPPED_HEADERS: readonly string[]; /** * Returns a copy of `headers` with the credential-bearing entries removed, * preserving the caller's `HeadersInit` shape (`Headers`, entry pairs, or a * plain record). Never mutates its argument. */ export declare function stripSensitiveHeaders(headers: HeadersInit | undefined, sensitiveHeaders: readonly string[] | undefined): HeadersInit | undefined; /** * Applies {@link stripSensitiveHeaders} to a request init when the next hop is * cross-origin; returns `init` unchanged otherwise. * * Callers thread the result forward as the init for every remaining hop, so a * header stripped once stays stripped for the rest of the chain even if a later * Location returns to the original origin. That is deliberate: restoring it * would let a `vendor -> attacker -> vendor` chain launder the credential back * into a request whose path the attacker chose. */ export declare function applyCrossOriginHeaderStrip(init: T, fromUrl: string, toUrl: string, sensitiveHeaders: readonly string[] | undefined): T; /** * Entity headers that describe a request body. Dropped alongside the body when * a redirect downgrades a request to a bodiless GET. * * This is load-bearing rather than cosmetic: leaving `Content-Length: 47` on a * request that now carries no body makes undici state a length no body * satisfies. */ export declare const REDIRECT_BODY_HEADERS: readonly string[]; /** * Applies the redirect's method/body rules to a request init, returning the init * for the next hop. * * Without this, every hop replayed the original method and body verbatim, so a * POST whose body carries a `client_secret` was re-POSTed to whatever origin the * previous server's Location header named — the header strip protects headers * only, and a body is not a header. * * - **303 (any method but HEAD), and 301/302 on POST** downgrade to a bodiless * `GET`. That is what the Fetch spec, browsers, `curl -L` and undici's own * follow mode all do, so matching it is compatibility rather than a policy of * ours. * - **307/308 (and 301/302 on a non-POST)** preserve method and body by * definition. Crossing origins with a body then THROWS * {@link FetchUrlErrorCode.REDIRECT_BODY_NOT_REPLAYED} rather than silently * downgrading: a downgrade would suppress the leak but also silently not * perform the write the caller asked for, and then report success. * * Otherwise `init` is returned unchanged (identity), so a same-origin 307 keeps * its method, body and `Content-Type`. */ export declare function applyRedirectMethodAndBody(init: T, status: number, fromUrl: string, toUrl: string): T; export type SafeFetchFn = (url: string, options: SafeFetchOptions) => Promise; /** * Register a platform-specific SafeFetch implementation. The Node/Bun * entrypoints call this at module load time to install the DNS-resolving, * connection-pinning implementation from `SafeFetch.server.ts`. * * Returns the previously registered implementation so callers can safely * restore it after a temporary override. */ export declare function registerSafeFetch(fn: SafeFetchFn): SafeFetchFn; export declare function getSafeFetchImpl(): SafeFetchFn; /** Restores the default browser-safe implementation. */ export declare function resetSafeFetch(): void; /** * SSRF-aware fetch. See {@link SafeFetchOptions} for the `allowPrivate` flag. * Throws `PermanentJobError` if the URL targets a private host without * permission, or (server impl) if DNS resolves to a private IP. */ export declare function safeFetch(url: string, options?: SafeFetchOptions): Promise;