Skip to content

outward

The confused deputy

Why fetching a URL somebody handed you is not the same as them fetching it.

Server-side request forgery is not really about requests. It is about position. Your server sits somewhere the person calling it does not — inside a network, with an identity, next to services that trust anything that can reach them — and a handler that fetches a URL it was handed lends that position out.

This fixture network has an inside. There is an admin service on an internal name, a cache on loopback, and a metadata endpoint that hands credentials to anything that asks from within. None of them is reachable from where you are sitting.

  • GET /fetch?url=https%3A%2F%2Fapi.partner.example%2Fstatus

    A host both builds are happy to call.

  • GET /fetch?url=http%3A%2F%2F169.254.169.254%2Flatest%2Fmeta-data%2F

    The address every cloud provider puts credentials behind. Compare what each build did with it.

The obvious defence is a deny-list: refuse localhost, refuse 127.0.0.1, refuse 169.254. It fails for the reason deny-lists always fail — it has to be right about every address that could ever reach inside, and it is competing with decimal notation, IPv6, DNS names that resolve inward, and a redirect from a host that was on nobody's list.

An allow-list has to be right about the three hosts you actually meant. That is a much smaller thing to be right about, and it is the whole of the difference.

  • GET /fetch?url=http%3A%2F%2Fredirector.example.com%2F

    A perfectly ordinary public host. Watch where the request actually ended up.

This build

hooks.example.com

Nothing sent yet. Every request goes to the application in this tab and nowhere else.