/** * SSRF protection for server-side fetches (content import, remote-asset * fetches, federation actor/collection fetches). * * Canonical home (session 148, federation-hardening Item 5). `@commonpub/ * server` re-exports `isPrivateUrl`/`safeFetch`/`safeFetchBinary`/ * `SafeFetchOptions` from here so its public API (stable since 2.48.0) * is unchanged for external consumers; `actorResolver.ts` imports * `isPrivateUrl` from here so the previously-diverged copy is gone. * * Two layers of defence: * 1. `isPrivateUrl` — synchronous string/literal-IP check (no DNS). * 2. A pinned-lookup undici dispatcher — resolves the hostname once, * rejects if ANY resolved address is private/reserved, and connects * to the validated address. This closes the DNS-rebinding TOCTOU * that the string check alone cannot (a public name whose A/AAAA * record points at a private IP). */ import dns from 'node:dns'; /** Classify a literal IP (v4, v6, or IPv4-mapped IPv6) as private/reserved. */ export declare function isPrivateIp(ip: string): boolean; /** * Synchronous, string-level SSRF check. Blocks malformed URLs, non-HTTP(S) * schemes, blocked hostnames, literal private/reserved IPs (v4, v6, * IPv4-mapped), and numeric-encoding bypasses. * * This does not resolve DNS — a public hostname whose A/AAAA record points * at a private address (DNS-rebinding) is NOT caught here. That gap is * closed by the pinned-lookup dispatcher used by `safeFetch`/ * `safeFetchBinary` (which validate every resolved address and connect to * the validated IP). Callers that fetch with their own client (e.g. * `resolveActor`'s injected `fetchFn`) still get this per-hop string check. */ export declare function isPrivateUrl(urlString: string): boolean; /** * Custom DNS lookup for the SSRF dispatcher. We resolve the hostname * ourselves (`all`, `verbatim`), reject if ANY returned address is * private/reserved (fail-closed — a rebinding resolver can return a * mix), then hand the validated address LIST back. * * undici's connector invokes the custom `lookup` with `all` semantics * and expects `callback(err, LookupAddress[])` (an array of * `{ address, family }`) — NOT the classic `(err, address, family)` * single form. Returning the single form makes undici read * `addresses[0].address` off a string → `ERR_INVALID_IP_ADDRESS: * undefined` and every fetch fails. Verified empirically against * undici 7 (see ssrf.integration.test.ts). Because every returned * address is pre-validated and the connection pins to one of them, * there is no second resolution between check and connect (TOCTOU * closed). */ export declare function pinnedLookup(hostname: string, options: dns.LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: dns.LookupAddress[]) => void): void; export interface SafeFetchOptions { accept?: string; userAgent?: string; timeoutMs?: number; /** Externally-owned abort signal; when set, the helper does not create its own timeout. */ signal?: AbortSignal; /** HTTP method (default GET). For signed AP requests. */ method?: string; /** Extra request headers, merged over the defaults (User-Agent/Accept). */ headers?: Record; /** Request body (signed AP POST). */ body?: string | Uint8Array; /** * Follow 3xx redirects (default true). Signed requests MUST pass * `false` — replaying a signed body/headers to a redirect target is * both invalid (the signature covers the original target) and a * confused-deputy risk. */ followRedirects?: boolean; } /** * Fetch a URL with SSRF protection (string check + pinned-lookup * dispatcher), redirect re-validation, a streaming size cap, and a * deadline covering the whole exchange. Returns the body as a string. */ export declare function safeFetch(url: string, options?: SafeFetchOptions): Promise<{ html: string; finalUrl: string; }>; /** * Like `safeFetch` but returns the body as a Buffer plus the upstream * Content-Type. Use for binary content (images, etc.). */ export declare function safeFetchBinary(url: string, options?: SafeFetchOptions): Promise<{ buffer: Buffer; contentType: string; finalUrl: string; }>; /** Buffered response shape returned by `safeFetchResponse` / `safeFetchSigned`. */ export interface SafeFetchResponseResult { ok: boolean; status: number; statusText: string; /** Response headers, lowercased keys. */ headers: Record; /** Already-streamed body, size-capped at MAX_RESPONSE_SIZE. */ body: Buffer; /** Convenience accessor for the upstream `content-type` header. */ contentType: string; finalUrl: string; } /** * SSRF-safe fetch that returns the response shape (status + buffered body) * without throwing on non-2xx, defaults `followRedirects: false`, and * applies the same pinned-dispatcher + per-hop URL validation as * `safeFetch`/`safeFetchBinary`. * * Use for callers that need the status code (federation delivery's * circuit-breaker) or that mint pre-signed HTTP-Signature requests * (signed bodies/headers must not replay to redirect targets). * * Body is fully consumed under the deadline + 10MB cap; the caller * decodes/parses synchronously off the returned `body` Buffer. */ export declare function safeFetchResponse(url: string, options?: SafeFetchOptions): Promise; /** * Forward a pre-signed `Request` (HTTP Signatures) through `safeFetchResponse`. * `followRedirects` is forced false — the signature covers the original * target and replaying it to a redirect target invalidates the signature * (and is a confused-deputy risk). All headers on the signed Request * are forwarded as-is; the body is read once (consuming the Request). */ export declare function safeFetchSigned(signedRequest: Request, options?: Omit): Promise; //# sourceMappingURL=ssrf.d.ts.map