SSRF via PDF renderers: your invoice generator is an internal proxy
Headless browsers rendering user HTML into PDFs will happily fetch cloud metadata endpoints. Two clients, same bug, one week. Here is the checklist.
Headless browsers rendering user-supplied HTML into PDFs will happily fetch URLs of their choosing — including internal URLs and cloud metadata endpoints. Two clients, same bug, one week apart.
What happens
You have an invoice generation endpoint. The user submits HTML via a WYSIWYG or template editor, and your app renders it server-side with Puppeteer or Playwright into a PDF. The user controls a img src="..." or a <link> in that HTML, so they can instruct the browser to load any resource.
If the headless browser has network access (and it does by default) it will hit http://169.254.169.254 or http://localhost:6379 or http://db.internal:5432 — exactly what SSRF vulnerability scanners look for.
What we recommend
- Disable all external network access in the headless browser. Pass the
--disable-network-accessflag to Chromium or configure Puppeteer'sargsto disable fetch. Many libraries have a--no-sandboxflag that works in combination with a sandbox or namespace. - Use a user namespace, not root. Run the headless browser in an unprivileged namespace, so even if SSRF is achieved, the process can only reach a small subset of the host.
- Whitelist permitted URLs only. If the HTML must contain images, validate them against an allowlist or a safe URL pattern and sanitize the
srcattribute before rendering. - Isolate the renderer from internal services. Even if the browser cannot reach the host network, it can still resolve and fetch
http://internal. Put the renderer on a network segment with no route to internal services.
The checklist
If you use Puppeteer, Playwright, or similar:
- [ ]
--disable-network-accessis set - [ ]
--disable-features=VizDisplayCompositoris set to mitigate CVEs in the renderer - [ ] The container or process is non-root
- [ ] Egress is limited by network policy
- [ ] HTML is sanitised before passing to the renderer
This is one of the highest-return checks we make during pentests.