Server-side request forgery is when your server fetches a URL that a user controls. Webhook configuration, image imports, link previews, PDF generation, anything taking a URL as input.
The risk is that your server sits inside a network the user can't reach. Cloud metadata endpoints, internal admin panels, databases bound to localhost, all reachable from your server, none from the internet.
Allowlist first
If you can enumerate the hosts you need to reach, do that and stop.
This covers most real cases. Webhooks to arbitrary customer endpoints are the main exception.
Why blocklists fail
The instinct is to reject localhost and 127.0.0.1. That doesn't hold, because the same address has many representations: decimal 2130706433, octal, IPv6 [::1], IPv4-mapped IPv6, a domain that resolves to a private address, a shortened URL that redirects to one.
You cannot enumerate the ways to write an address. You can check what an address actually resolves to.
Resolve, then check
Note 169.254.169.254 specifically. That's the cloud metadata endpoint on AWS, GCP and Azure, and on IMDSv1 it hands out credentials to anything that asks. If you're on AWS, enforce IMDSv2. It requires a header and a token, which an SSRF through a simple GET can't supply.
Redirects
Validating the URL you were given and then following redirects means validating nothing. The first response can point anywhere.
Handle redirects yourself, re-validating each hop, or disable them.
There's also a race between the DNS check and the request, a hostname can resolve differently the second time. Pinning the request to the IP you validated closes it, at the cost of some complexity.
The other layer
Network egress rules are the durable fix. A service that doesn't need to reach the internal network shouldn't be able to, regardless of what the application code does.
Application-level validation and network-level restriction are both worth having. The second one survives someone forgetting the first.