SSRF-Safe URL Admission

String validation is not SSRF protection. Authorization must apply to the address actually connected after DNS resolution and every redirect.

Security · TypeScript

SSRF-Safe URL Admission

Redirects are new destinations, not continuations of the original authorization.

// Admission returns the address the transport MUST connect to.
async function authorize(input: string) {
  const url = new URL(input)
  if (url.protocol !== 'https:') throw new Error('protocol denied')
  const hostname = url.hostname.replace(/^\[|\]$/g, '')
  const family = net.isIP(hostname)
  const answers = family
    ? [{ address: hostname, family }]
    : await dns.promises.lookup(hostname, { all: true, verbatim: true })
  const allowed = answers.filter(({ address }) => isPublicRoutable(normalizeIp(address)))
  if (!allowed.length) throw new Error('destination denied')
  return { url, hostname, ...allowed[crypto.randomInt(allowed.length)] }
}

const target = await authorize(input)
const request = https.request({
  hostname: target.address, family: target.family,
  port: target.url.port || 443,
  path: target.url.pathname + target.url.search,
  headers: { host: target.url.host },
  servername: net.isIP(target.hostname) ? undefined : target.hostname,
  rejectUnauthorized: true, signal: AbortSignal.timeout(2_000),
})
request.end()
// Disable automatic redirects. Re-run authorize() for every Location.
// Enforce a response-byte ceiling before buffering the body.
// A proxy can bypass this pin: enforce the same policy at actual egress.

Invariant: Every network destination actually connected to is authorized before the connection is established.

Use when: Users can provide a webhook or fetch target.

Why this boundary matters

Validating a DNS answer is meaningless if the HTTP client resolves again. Authorization must bind the selected address to the actual socket connection.

Failure policy

BoundaryAction
Protocol not allowedReject before resolution
Resolved address is private or reservedReject
No authorized address remainsReject
Redirect returnedTreat it as a new authorization decision
DNS answer changesRe-resolve and connect only to an authorized address
IPv4/IPv6 mismatchNormalize and validate both address families
Proxy or service mesh involvedEnforce policy at the actual egress boundary
Timeout exceededAbort
Response exceeds size limitAbort and discard

Trade-offs

Hostname and IP deny lists are defense-in-depth, not a complete boundary. DNS rebinding, redirects, IPv6, IPv4-mapped IPv6, proxies, service meshes, cloud metadata endpoints, and alternate IP representations make them fragile. Prefer destination allowlists; use an isolated egress proxy or network policy when arbitrary destinations are required.

Decision rule: Do not expose a general-purpose server-side fetcher to untrusted input. Constrain destination, protocol, resolution, redirects, time, response size, and egress.

Further reference

Browse all engineering snippets

>