# Treat every user-supplied URL as a network request

> A valid-looking URL can resolve or redirect into a private network. Protect outbound fetches with DNS checks, redirect controls, timeouts, and limits.

Source: https://saascode.ai/inside/ssrf-kb-url-parser · Published: 2026-08-21 · Section: academy

---
The feature says “import this page.” The server hears “make a network request to an address chosen by the user.” Those are not the same risk.

One live knowledge importer called `fetch(sourceUrl)` directly. An authenticated user could supply a loopback address, a private-network host, or the link-local address used by cloud metadata services. The code already had a public-URL guard on six other outbound paths. This seventh path simply missed it.

Authentication does not make that safe. A paying user must not be able to turn your server into a probe for services they cannot reach directly.

## A URL string is not a destination

Checking for `http:` or `https:` is only the first step. Hostnames resolve after validation, and redirects can send an apparently public request somewhere private. A production-safe fetch boundary should:

1. parse with a real URL parser and allow only the protocols and ports you support;
2. reject embedded credentials and malformed hostnames;
3. resolve every A and AAAA result, then reject loopback, link-local, private, multicast, unspecified, and other non-public ranges;
4. connect to the checked address or otherwise prevent a second DNS lookup from changing the destination;
5. disable automatic redirects, or validate and resolve every redirect target before following it;
6. enforce a short timeout, a response-size cap, and a content-type policy.

Prefer a strict hostname allowlist when the product only needs a known set of services. If arbitrary public URLs are the feature, a denylist of private ranges is necessary but more fragile. OWASP explicitly warns about DNS pinning and unsafe redirects in SSRF defenses.

## Put the guard in the transport

This is safer than asking every feature to remember a preflight call:

```ts
const response = await fetchPublicUrl(sourceUrl, {
  maxRedirects: 3,
  timeoutMs: 8_000,
  maxBytes: 2_000_000,
})
```

`fetchPublicUrl()` should own parsing, DNS resolution, address checks, redirect handling, and limits. A separate `validatePublicUrl()` followed by a normal `fetch()` can still leave a DNS-rebinding gap between the check and the connection.

Also treat the response as untrusted. Do not forward arbitrary response headers, do not reflect fetched HTML into your own origin, and do not keep reading an unlimited body.

## Find the missing seventh path

Search every outbound request whose URL can be influenced by a user: document importers, link previews, RSS readers, webhook testers, image proxies, and unfurlers. Then verify that each goes through the same guarded transport.

The original bug was one missing call in a codebase that already protected six similar paths. That is why a central transport boundary beats a checklist: the seventh caller should not get a choice.
