PDF Generators: SSRF and XSS Bugs That Still Pay
“Export to PDF” and “render invoice” features often spin up headless Chromium, wkhtmltopdf, or a homegrown HTML template engine. Whatever HTML you can influence becomes a browser with server-side privileges. That is why PDF sinks keep producing SSRF and stored XSS years after teams “fixed user HTML.”
Why this sink is special
The renderer runs on an app server or worker with:
- Access to internal URLs and metadata endpoints
- File system visibility (
file:///etc/passwdon older stacks) - A JavaScript engine if the engine is browser-based
Your injection point might be a rich-text field, a custom logo URL, an HTML email body that gets archived as PDF, or a “header HTML” admin setting.
PortSwigger’s SSRF labs translate cleanly here: replace “URL fetch parameter” with “resource loaded during PDF print.”
SSRF playbook
- Find a field reflected into the PDF HTML (
img src,iframe,link href, CSSurl()). - Point it at a collaborator HTTPS URL — confirm the worker fetches.
- Move to internal targets allowed by scope:
http://127.0.0.1:PORT, cloud metadata (watch IMDSv2 constraints), staging admin panels. - Note whether redirects and non-HTTP schemes work.
Blind SSRF with DNS/HTTP callbacks still counts when you can show internal reach. File-scheme reads are rarer on modern Chromium but appear on legacy wkhtmltopdf.
XSS and “PDF XSS” reality check
Script that runs inside the generator may not become browser XSS for an end user. Impact paths that do pay:
- HTML stored and later viewed in a web preview as HTML (not only PDF bytes)
- PDF viewers that execute risky launch actions (less common; verify)
- SSRF/local file read via the generator — usually the real severity
- Phishing: crafted PDFs that look like official invoices from the brand’s domain download path
Do not claim “critical stored XSS” if the only execution context is a locked-down headless print with no exfil channel. Show a collaborator hit from script, an internal HTTP body reflected into the PDF, or a sensitive file: read instead.
Report hygiene
Include the exact HTML snippet, the generated PDF (or a page screenshot), and out-of-band proof. Recommend: sanitize HTML to a safe subset, block private IP ranges at the renderer egress, disable JS in the PDF engine if feasible, and never pass raw user HTML to privileged workers.
Whenever I see “Download PDF,” I assume an HTML browser is hiding behind the button until proven otherwise.