File size: 1,991 Bytes
6f91ce5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | import { lookup } from 'node:dns/promises'
import { BlockList, isIP } from 'node:net'
// URLs that come from users (pasted bookmarks, paper links) must never make the server
// request itself, the cloud metadata service or anything else on a private network.
// BlockList also checks IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) against the IPv4 rules.
const blocked = new BlockList()
for (const [net, prefix] of [
['0.0.0.0', 8],
['10.0.0.0', 8],
['100.64.0.0', 10],
['127.0.0.0', 8],
['169.254.0.0', 16],
['172.16.0.0', 12],
['192.0.0.0', 24],
['192.168.0.0', 16],
['198.18.0.0', 15],
['224.0.0.0', 4],
['240.0.0.0', 4],
] as const) {
blocked.addSubnet(net, prefix, 'ipv4')
}
for (const [net, prefix] of [
['::', 128],
['::1', 128],
['fc00::', 7],
['fe80::', 10],
['ff00::', 8],
] as const) {
blocked.addSubnet(net, prefix, 'ipv6')
}
export async function assertPublicUrl(url: URL) {
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`Blocked protocol ${url.protocol}`)
}
const host = url.hostname.replace(/^\[|\]$/g, '')
const addresses = isIP(host) ? [{ address: host }] : await lookup(host, { all: true })
for (const { address } of addresses) {
if (blocked.check(address, isIP(address) === 6 ? 'ipv6' : 'ipv4')) {
throw new Error(`Blocked private address ${address} for ${url.hostname}`)
}
}
}
// fetch that checks the address of every hop in a redirect chain, not just the first URL
export async function publicFetch(input: string, init: RequestInit = {}, maxRedirects = 3) {
let url = new URL(input)
for (let hop = 0; ; hop++) {
await assertPublicUrl(url)
const res = await fetch(url, {
...init,
redirect: 'manual',
signal: init.signal ?? AbortSignal.timeout(5000),
})
const location = res.headers.get('location')
if (res.status < 300 || res.status >= 400 || !location) return res
if (hop >= maxRedirects) throw new Error(`More than ${maxRedirects} redirects`)
url = new URL(location, url)
}
}
|