Bug Hunting Journey

Writeups SSRF

SSRF · critical

The PDF renderer that talked to the cloud metadata service

Severity Critical CVSS 8.6
Impact
Instance metadata and IAM role credentials disclosed
Bounty
$2.5k - $5k
Program
Private

TL;DR

The customisable invoice template was rendered server-side by a headless browser that fetched remote resources with no restriction. An image tag pointing at the metadata endpoint was enough to retrieve temporary role credentials.

Any “customise your template” feature that ends in a PDF is server-side rendering wearing a hat. ███████████‘s was no exception.

Finding it

The template editor accepts a subset of HTML for the invoice header. <script> tags are stripped; <img> is not - which makes sense, since the whole point is to put a logo there. Rendering happens server-side, in headless Chrome.

Invoice template - payloadHTML
<img src="https://oast.████████.oastify.com/ping" width="1" height="1">

The DNS callback lands twelve seconds after the PDF is generated, from the cloud provider’s address range. So outbound network access is open, and rendering really does happen inside their infrastructure, not on the client.

Escalation

Confirmation - reading instance metadatabash
# the generated PDF contains whatever the metadata endpoint returns<img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" >

The PDF comes back with the role name spelled out. A second render against the full path returns the temporary credentials. I stop at proof: I make no API call with those credentials, I include them truncated in the report, and I ask for immediate revocation.

Takeaways

  • Any server-side HTML rendering is an HTTP client. PDFs, thumbnails, link previews, importers: the question is not whether it reaches the network, but where.
  • Stripping <script> protects nothing here. The attack goes through a resource, not through code.
  • IMDSv2 and a renderer-level domain allowlist would each have broken the chain independently.

Disclosure timeline

  1. Report sent
  2. Triaged handled in four hours, which is rare and worth saying
  3. Fix deployed IMDSv2 enforced and a domain allowlist added to the renderer
  4. Cleared for publication