A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * SSRF-guarded fetch for the read-through renderer.
3 *
4 * The renderer fetches hostnames derived from UNTRUSTED input — the `@handle` in the
5 * URL, a `did:web` host, and the PDS `serviceEndpoint` from a DID document. Without
6 * guarding, a request like `/@169.254.169.254/x` or a `did:web` pointing at an internal
7 * host would make the server fetch loopback / cloud-metadata / private addresses (SSRF).
8 *
9 * `assertSafeUrl` allows only `https://` to syntactically-valid **public** domains —
10 * rejecting IP literals, single-label/`localhost`, and reserved/internal TLDs — and
11 * `safeFetch` additionally refuses to follow redirects (which could pivot to an internal
12 * host).
13 *
14 * Residual risk: a public domain whose DNS resolves to a private IP (DNS rebinding).
15 * That can't be closed portably without resolving DNS and inspecting the address; it is
16 * best handled at the network/egress layer of the deploy host (tracked for SP7).
17 */
18
19const RESERVED_TLDS = new Set( [
20 'localhost', 'local', 'internal', 'intranet', 'lan', 'home', 'corp',
21 'test', 'example', 'invalid', 'arpa', 'onion', 'localdomain',
22] );
23
24/** True only for a syntactically valid, public, non-IP domain name. */
25export function isPublicHostname( hostname: string ): boolean {
26 if ( ! hostname ) {
27 return false;
28 }
29 const host = hostname.toLowerCase().replace( /\.$/, '' );
30 if ( host.includes( ':' ) || host.includes( '[' ) ) {
31 return false; // IPv6 literal or embedded port
32 }
33 if ( /^\d{1,3}(\.\d{1,3}){3}$/.test( host ) ) {
34 return false; // IPv4 literal
35 }
36 const labels = host.split( '.' );
37 if ( labels.length < 2 ) {
38 return false; // single-label (localhost, etc.)
39 }
40 for ( const label of labels ) {
41 if ( ! /^[a-z0-9-]{1,63}$/.test( label ) || label.startsWith( '-' ) || label.endsWith( '-' ) ) {
42 return false;
43 }
44 }
45 const tld = labels[ labels.length - 1 ];
46 if ( /^\d+$/.test( tld ) || RESERVED_TLDS.has( tld ) ) {
47 return false;
48 }
49 return true;
50}
51
52/** Parse + validate a URL for outbound fetching; throws if it isn't a safe public https target. */
53export function assertSafeUrl( rawUrl: string ): URL {
54 let url: URL;
55 try {
56 url = new URL( rawUrl );
57 } catch {
58 throw new Error( `Invalid URL: ${ rawUrl }` );
59 }
60 if ( url.protocol !== 'https:' ) {
61 throw new Error( `Refusing non-https URL (${ url.protocol })` );
62 }
63 if ( ! isPublicHostname( url.hostname ) ) {
64 throw new Error( `Refusing non-public host: ${ url.hostname }` );
65 }
66 return url;
67}
68
69/** `fetch` restricted to safe public https targets, without following redirects. */
70export function safeFetch( rawUrl: string, init: RequestInit = {} ): Promise< Response > {
71 const url = assertSafeUrl( rawUrl );
72 return fetch( url, { ...init, redirect: 'manual' } );
73}