PDF Renderer SSRF: Blocklisted Metadata, a Redirect, and a Stop Condition
You are testing a document-conversion SaaS. A feature accepts a URL and renders it server-side to PDF via POST /api/render {"url":"https://..."}. The render worker runs on AWS Fargate with an IAM task role attached. You confirm the worker fetches arbitrary external URLs by pointing it at a listener you control; the request arrives from 198.51.100.23 with a headless-Chrome User-Agent. You then submit http://169.254.169.254/latest/meta-data/ and receive a 403 with the message "URL not permitted". The variants http://[::ffff:169.254.169.254]/, http://169.254.169.254.nip.io/ and http://0xA9FEA9FE/ return the same 403. The rules of engagement forbid accessing customer data and require you to stop at the first evidence of credential access.
1. Design a proof of concept that proves the metadata service is reachable without retrieving credentials.
Read the worked answer
The 403 proves the application filter rejected your string. It does not prove the worker cannot reach link-local space - so the next step is to move the decision point away from the submitted URL and into a redirect you control.
Host https://oob.attacker.example/hop and have it return 302 Location: http://169.254.169.254/latest/meta-data/. Submit the OOB URL. Three outcomes are distinguishable: (a) the app rejects the OOB URL โ redirects are re-validated at submission time only, no proof yet; (b) the worker fetches the OOB URL but does not follow the redirect โ note the behaviour, try a meta-refresh or a JS location= in the rendered page if Chrome executes it; (c) the worker follows and the resulting PDF differs from a control render (an IMDSv2 401 Unauthorized body instead of a network error or your own page).
Outcome (c) is the clean proof: a 401 from the metadata service means the request arrived at IMDS. Record the response status, the timing delta versus a control URL, and the OOB hit log. Never render /latest/meta-data/iam/security-credentials/ into a PDF - if IMDSv1 were enabled, the credential would land in a file on your disk, which is exactly the stop condition in the RoE.
2. What do the failed bypasses tell you about the control, and what would you check next before concluding it is robust?
Read the worked answer
The fact that [::ffff:169.254.169.254], nip.io and the hex form all return the same 403 is informative: a naive string blocklist would have let at least one through, so the filter is probably resolving the hostname to an IP and range-checking against link-local/private space. That is a materially better control and you should say so in the report.
The remaining gap is the one a range check usually misses: the check happens on the submitted URL, not on every hop. So test redirect following specifically - first to an allowed public host that then 302s to metadata, to see whether the fetcher follows at all; then to metadata, to see whether the check is re-applied. If redirects are re-validated, the next question is DNS rebinding: does the worker resolve the name once and pin the IP for the fetch, or resolve again after validation? A record with TTL 0 alternating between a public IP and 169.254.169.254 answers that, but only if validation and fetch are separate lookups.
Also check the other metadata endpoint: on ECS/Fargate, http://169.254.170.2/v2/credentials/<relative-uri> serves task credentials, and the AWS_CONTAINER_CREDENTIALS_RELATIVE_URI variable is often reachable from the same worker. If both IMDS and the ECS endpoint are blocked, the control is solid - report it as a strength, not a gap.
3. Suppose the redirect works and the rendered PDF contains a valid `AccessKeyId` and `Token`. What happens next?
Read the worked answer
You stop, immediately. The RoE's stop condition is triggered and nothing in the engagement authorises using those credentials.
Concretely: do not call any AWS API to "confirm" the keys, do not enumerate the role's permissions, do not check sts:GetCallerIdentity. Verification is exactly the step that converts a read-only rendering bug into unauthorised access to the client's cloud account. Delete the PDF containing the token, empty the trash, and record in your log only that a credential was rendered, the timestamp, and the request that produced it.
Then escalate the same day through the incident channel, not the report: state that the Fargate task role's temporary credentials were retrievable via the render endpoint, and recommend immediate rotation of the task role, enforcement of IMDSv2 with a hop limit of 1, and egress filtering from the worker so it cannot reach 169.254.0.0/16. The report carries a redacted excerpt (endpoint, parameter, redirect chain) and an explicit sentence: "credentials were observed in the rendered output and were not used." That sentence protects both you and the client's audit trail.