SSRF Through an Allowlisted Image Fetcher
An application lets users import a profile image by URL at POST /api/import-image with JSON {"url":"https://www.trusted-cdn.com/avatar.png"}. The server fetches the URL, resizes the image, and returns a PNG. Validation requires the hostname to end with .trusted-cdn.com and resolves the hostname once, rejecting private IPs. The fetcher follows up to three redirects. On upstream failure, the error response includes the first 200 characters of the upstream body. You find an open redirect at https://www.trusted-cdn.com/go?to=. Burp Collaborator is available. The target is a cloud instance with metadata at http://169.254.169.254/latest/meta-data/iam/security-credentials/. Direct requests to 169.254.169.254 are rejected with 400 'blocked host'. The open redirect accepts http and https URLs and returns a 302. The image resizer only accepts content-type image/png, but the error path runs before content-type validation.
1. How do you confirm SSRF with Burp Collaborator, and why might a direct request to 169.254.169.254 fail?
Read the worked answer
Set the JSON url to a Collaborator payload such as http://<collaborator>/probe.png and send it in Repeater. A DNS and HTTP interaction in Collaborator confirms the server makes outbound requests. A direct request to http://169.254.169.254/latest/meta-data/ fails because the validator resolves the hostname and rejects private or link-local addresses before fetching. The Collaborator host is public, so it passes. This distinguishes SSRF from a local-only fetch. Also test an allowlisted host with a path that returns 404; the error body may reveal whether the fetcher reached the host. If Collaborator shows only DNS and no HTTP, the fetcher may resolve but not connect; investigate egress filtering. Record the exact JSON body and the Collaborator interaction timestamps.
2. Construct the bypass chain and explain why the redirect defeats the allowlist.
Read the worked answer
Use https://www.trusted-cdn.com/go?to=http://169.254.169.254/latest/meta-data/iam/security-credentials/. The initial hostname ends with .trusted-cdn.com, so the allowlist passes and the resolved IP is public. The fetcher then receives a 302 from the open redirect and follows it to the link-local metadata address. The validation is only applied to the initial URL, not to each redirect target, so the private-IP check is bypassed. Because the error path returns the first 200 characters of the upstream body, request a metadata path that returns text. If the first request returns a role name, request /latest/meta-data/iam/security-credentials/<role> to retrieve credentials. Confirm with Collaborator first using a redirect to a Collaborator URL to prove redirect-following works.
3. What fix and verification would you recommend?
Read the worked answer
Disable redirects in the fetcher, or revalidate every hop: resolve the hostname, reject private, link-local, and loopback addresses, and pin the resolved IP for the actual connection to prevent DNS rebinding. Use an allowlist of exact hosts and paths, not suffixes, and block the metadata IP at the network egress. Do not return upstream bodies in error messages. Verify by replaying the open-redirect chain; it should fail with a generic 400 and no Collaborator interaction. Test a legitimate allowlisted image to confirm functionality. Also test decimal and octal IP encodings and a DNS name that resolves to 169.254.169.254 to ensure the IP check runs after resolution, not just on the hostname string.