chaosbyte
← All posts
criticalMay 27, 2026·5 min read

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

  1. Disable all external network access in the headless browser. Pass the --disable-network-access flag to Chromium or configure Puppeteer's args to disable fetch. Many libraries have a --no-sandbox flag that works in combination with a sandbox or namespace.
  2. 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.
  3. Whitelist permitted URLs only. If the HTML must contain images, validate them against an allowlist or a safe URL pattern and sanitize the src attribute before rendering.
  4. 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-access is set
  • [ ] --disable-features=VizDisplayCompositor is 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.