UNPKG

@accounter/server

Version:
55 lines (54 loc) 2.73 kB
/** * Fetch a document from a caller-supplied URL, safely. * * A server that fetches arbitrary URLs on request is an SSRF primitive: it sits * inside the private network and will happily read the cloud metadata endpoint, * an internal admin panel, or `localhost` on behalf of whoever asked. Every * guard below exists for that reason, and each one is re-applied **after every * redirect** — checking only the submitted URL is the classic way this goes * wrong, since the attacker controls the redirect target too. * * Google Drive share links are *not* handled here; they are not download links * (`/file/d/<id>/view` answers with an HTML page) and need the Drive API. The * resolver routes those to `GoogleDriveProvider` before reaching this helper. */ /** Max bytes accepted from a remote document. */ export declare const MAX_REMOTE_DOCUMENT_BYTES: number; /** Max redirects followed before giving up. */ export declare const MAX_REDIRECTS = 5; /** Wall-clock budget for one document fetch, including redirects. */ export declare const FETCH_TIMEOUT_MS = 30000; /** * Accepted content types, matched against the *response's* `Content-Type` — not * the URL's extension and not anything the caller claims. A `.pdf` URL that * answers with `text/html` is a login page or an error, and storing it would * file a web page as a financial record. */ export declare const ALLOWED_REMOTE_MIME_TYPES: Set<string>; /** Thrown for every refusal here, so the resolver can report it per URL. */ export declare class RemoteDocumentError extends Error { } /** * Whether a literal IP address belongs to a range that must never be reachable * through this helper: loopback, private, link-local (which includes the cloud * metadata address 169.254.169.254), carrier-grade NAT, and the IPv6 * equivalents. */ export declare function isBlockedIp(address: string): boolean; /** * Reject a URL before it is fetched. Applied to the submitted URL and again to * every redirect target. * * Hostnames that are not literal IPs are checked by name only. Resolving them * here would not close the DNS-rebinding gap either (the name is resolved again * by `fetch`), so the meaningful guard is the response-side one: a redirect to * an internal host still has to pass this check, and the content-type check * rejects whatever an internal service would answer with. */ export declare function assertFetchableUrl(rawUrl: string): URL; /** * Fetch one document. Redirects are followed manually so each hop can be * re-validated; `fetch`'s own `redirect: 'follow'` would hide the intermediate * targets and defeat {@link assertFetchableUrl}. */ export declare function fetchRemoteDocument(rawUrl: string): Promise<File>;