@accounter/server
Version:
Accounter GraphQL server
55 lines (54 loc) • 2.73 kB
TypeScript
/**
* 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>;