const BLOCKED = ["localhost", "127.0.0.1", "169.254.169.254"];
async function fetchPreview(rawUrl: string) {
const url = new URL(rawUrl);
if (BLOCKED.includes(url.hostname)) throw new Error("Blocked");
return fetch(url); // follows redirects by default
}
https://attacker.example/r, which answers with a 302 redirect to http://169.254.169.254/latest/meta-data/The check only looks at the first URL. fetch then follows the redirect to the metadata address without checking it. A hostname whose DNS record points at 169.254.169.254 gets past the list too. Fix: resolve the name and check every IP against private and link-local ranges, use redirect: "manual" and check each hop, and connect to the IP you checked. On AWS, also require IMDSv2. It needs a PUT request with a special header to get a token first, which a simple fetched URL can't send.
"http://2852039166/" is the tempting wrong answer. It is the same address, but new URL turns it into 169.254.169.254, so the list catches it. new URL also lowercases the scheme. The port doesn't change the hostname.
Read more
- Server Side Request Forgery Prevention Cheat Sheet (OWASP) (https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)cheatsheetseries.owasp.org
- Instance metadata service (AWS) (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)docs.aws.amazon.com