skip to content

Your backend service calls third-party URLs with an HTTP client that automatically follows redirects. What controls would you place on that redirect-following behaviour before shipping it?

level: seniorimportance: should knowfreq 30%

answer

  1. Redirect target is chosen by the remote server, not you
  2. Re-run URL validation per hop, not once
  3. Resolve -> validate IP -> connect to that IP (DNS rebinding)
  4. Hop cap + one deadline for the whole chain + size cap
  5. Safest: disable auto-follow, handle Location yourself

basics

~20 s

Cap the number of hops, re-run your URL validation on every hop (a redirect bypasses the check you did on the first URL), refuse scheme downgrades and private or link-local addresses, verify credentials are dropped cross-origin, and apply a total time budget across the chain - or disable auto-follow and handle redirects yourself.

solid answer

~60 s

Auto-following makes the *server* choose your next request target, so every control you applied to the original URL must be re-applied per hop. 1. **Cap hops** - 3 to 5 is generous for an API; unbounded following is a hang and an amplification risk. 2. **Re-validate every hop.** Allowlist checks, blocked private ranges (`127.0.0.0/8`, `10/8`, `169.254.169.254`, IPv6 unique-local and mapped forms) must run on each redirect target, not just the first URL. Otherwise a public URL redirecting to the cloud metadata endpoint walks straight past your SSRF defence. 3. **Resolve-then-connect** to defeat DNS rebinding: validate the resolved IP and connect to that IP, rather than validating a hostname and letting a second lookup return something else. 4. **Refuse downgrades and odd schemes** - no https to http, and nothing outside http/https (`file:`, `gopher:`, `ftp:`). 5. **Confirm credential stripping** in your library, and never enable "trusted" credential forwarding for third-party URLs. 6. **Budget the whole chain** - one deadline covering all hops, plus a response size cap. 7. **Log the final effective URL**, since it may be nothing like the one requested. The safest default for untrusted URLs is to disable auto-follow and decide about each `Location` explicitly.

go deeper

for a junior

Know that following redirects means the remote server picks your next URL, so there must be a limit on hops and a check on where you end up.

for a middle

List concrete controls: hop cap, scheme restrictions, per-hop validation, confirming that credentials are dropped across origins.

for a senior

Own the SSRF story end to end - resolve-then-connect against rebinding, metadata-endpoint blocking on every hop, chain-wide deadlines and size caps, and negative tests for each.

for a principal

Push the control into architecture: an isolated egress path or fetch proxy for untrusted URLs, so a single validation gap is contained, plus policy on library defaults and their drift across versions.

## Why redirects deserve their own threat model When your service fetches a URL - a webhook target, an image the user linked, an OpenID discovery document - you typically validate that URL: right scheme, allowed host, not an internal address. Automatic redirect following silently discards that guarantee. The remote server, which may be attacker-controlled or merely compromised, now picks the next URL your service will request from inside your network perimeter. Validation performed once, on the first URL, protects nothing. ## Controls, in order of importance **Validate on every hop.** This is the whole game. The check that runs on the user-supplied URL must run again on each `Location`. A canonical attack: the user submits `https://harmless.example/x`; that host returns `302 Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/`; your service dutifully fetches cloud instance credentials and returns the body to the attacker. Blocking loopback, private (RFC 1918), link-local, carrier-grade NAT, and IPv6 equivalents including IPv4-mapped forms (`::ffff:169.254.169.254`) and unique-local addresses, on every hop, is the mitigation. **Resolve, validate, then connect to the resolved address.** Validating a hostname and then handing the hostname to the socket layer leaves a time-of-check/time-of-use window: DNS rebinding returns a public address to your validator and a private one to the connect. Resolve once, check the resulting IPs, connect to a checked IP with the original `Host` and SNI preserved. **Cap hops and set a chain-wide deadline.** Each hop is a fresh connection to a possibly slow host. A per-request timeout applied per hop multiplies by the hop count, so a 10-second read timeout with 20 hops is a 200-second hang holding a thread and a connection. Use one deadline for the whole operation, and a hop cap of a handful. **Cap response size, on every hop.** A redirect chain can end at a multi-gigabyte body or a decompression bomb; enforce a byte ceiling while streaming rather than after buffering. **Scheme discipline.** Follow only http and https, never a downgrade from https to http (which exposes the request to the network), and never non-HTTP schemes. Some clients historically followed `file:` URLs, turning a fetcher into an arbitrary file reader. **Credentials.** Confirm your library drops `Authorization`, cookies, and proxy credentials across origins - and never set curl's `--location-trusted` or a library's equivalent for third-party URLs. If you attach an internal service token to outbound calls, ensure it cannot ride a redirect to an external host. **Observability.** Log the requested URL, each hop, and the final effective URL, plus a metric for hop counts. Chains that suddenly get longer are a signal, and incident response needs to know what was actually fetched, not what was asked for. ## The stronger option: do not auto-follow For untrusted URLs, configure the client with redirect following **disabled** and handle 3xx responses yourself: read the `Location`, resolve it against the request URI, run the same validation pipeline used for the original input, decrement your own hop budget, and issue the next request explicitly. This costs a dozen lines and turns an invisible policy inside a library into code you can test - including negative tests asserting that a redirect to a private address is rejected. ## Related design considerations - **Egress isolation.** The strongest control is architectural: perform untrusted fetches from a network segment or proxy that has no route to internal services or metadata endpoints. Then a validation slip is not automatically an incident. - **Idempotency of the fetch.** Redirect-following re-sends non-idempotent requests when the method is preserved; for outbound webhooks, decide explicitly whether a redirected POST may be delivered twice. - **Library defaults drift.** Redirect limits, credential stripping and scheme rules differ across libraries and change across versions. Pin the behaviour with tests rather than trusting documentation.

  • Why does validating the hostname before connecting not fully prevent a redirect from reaching an internal address?
    Because of the gap between the check and the connection: DNS rebinding returns a public address when your validator resolves the name and a private one when the socket layer resolves it moments later. The mitigation is to resolve once, validate the resulting IP addresses, and connect directly to a validated IP while preserving the original Host header and SNI, so no second lookup can change the destination.
  • What is wrong with configuring a 10-second timeout per request and allowing up to 20 redirects?
    Timeouts compose multiplicatively with hops, so the worst case is 200 seconds of a held thread and connection for a single logical operation - well past any sane caller deadline and a cheap way to exhaust a pool. Redirect chains need one deadline for the whole operation plus a small hop cap, so the total time is bounded regardless of how many hops the remote side invents.

Vetting a visitor at the front desk, then letting them hand you a note saying "actually, go into the server room instead" - and walking in without a second check. The second door needs the same badge check as the first.

saying these in an interview costs you the question

  • Validating only the initial URL and trusting the library for the rest
  • Blocking only 127.0.0.1 and 10.0.0.0/8 while missing 169.254.169.254, IPv6 unique-local, and IPv4-mapped IPv6 forms
  • Allowing an https-to-http downgrade, or following non-HTTP schemes such as file:
  • Applying the timeout per hop instead of budgeting the entire chain
  • Enabling credential forwarding across hosts to 'make the integration work'

context