If Your App Renders User-Supplied HTML, You Have an SSRF Problem

It is a full browser proxy sitting inside your VPC with unrestricted network access, and anyone who can feed it HTML controls where it goes and what it…

It is a full browser proxy sitting inside your VPC with unrestricted network access, and anyone who can feed it HTML controls where it goes and what it fetches [1].

Why a headless browser is worse than a fetch client

A typical SSRF vulnerability in a REST client makes one outbound request to an internal IP and returns the response. That is dangerous, but bounded. A headless browser renderer loads a page, executes JavaScript, follows every redirect, and fetches every subresource—CSS, fonts, images, iframes, fetch() calls—each of which can hit a different target [1].

The renderer usually runs server-side inside your VPC, often with no egress controls at all. It has more network reachability than the application layer that invoked it [1]. Your Node.js API might only talk to your database and a few third-party services, but the Puppeteer or Playwright instance beside it can reach anything the host can: internal load balancers, cloud metadata endpoints, the Kubernetes API, other pods in the cluster.

In a documented attack, an attacker hosted a page on an otherwise-allowlisted domain. The page used JavaScript to fetch AWS instance metadata credentials and embedded them into the rendered PDF. The allowlist, timeout, and container isolation all failed because the browser executed attacker code [1].

Headless browsers behind rendering endpoints can read file:// paths, reach cloud metadata at 169.254.169.254, expose the Chrome DevTools Protocol on localhost:9222, and with --no-sandbox any XSS vulnerability escalates to remote code execution [2].

Attack vectors that bypass naive defenses

file:// access and local file exfiltration

A file:// iframe can read /etc/passwd, /app/.env, /root/.aws/credentials, and /proc/self/environ. Browsers auto-generate directory listings for file:// directories, so an attacker can browse the filesystem structure before reading specific files [2]. Playwright's bundled Firefox sets security.fileuri.strict_origin_policy=false, which lets a file:// document fetch other file:// resources and exfiltrate their contents [2].

Cloud metadata endpoints

Cloud metadata endpoints vary by provider: AWS at 169.254.169.254, GCP at metadata.google.internal with a Metadata-Flavor header requirement, Azure at 169.254.169.254/metadata with a Metadata header, and Alibaba at 100.100.100.200 [2]. AWS IMDSv2 requires a PUT request to obtain a token before the metadata GET, but a headless browser can execute this flow via fetch() just as a human attacker would with curl [2].

Redirect chains and allowlist bypasses

Input URL allowlists fail because redirects and open-redirect chains on trusted domains bypass them. A link to github.com/redirect?url=... or a similar construct on any allowlisted SaaS can chain to an internal IP or metadata endpoint [1]. The browser follows these automatically. Your application validates the first URL. The renderer visits the third or fourth hop.

Why common mitigations fail

Timeouts do not stop a fast JavaScript fetch() to a metadata endpoint [1]. Container isolation does not help when the browser runs in the same VPC as your database and internal services [1].

Network-layer blocking alone can miss IPv6 link-local addresses and other private-range equivalents [1].

IMDSv2 with hop limit 1 is the recommended AWS mitigation, but the PUT-plus-GET flow is still executable by JavaScript inside a headless browser [2].

PDF generation endpoints are a common bug bounty discovery path precisely because the attack surface is underrated. Tools like wkhtmltopdf, Headless Chrome, Puppeteer, and Playwright all render HTML that can reference internal resources, and security teams often treat them as document utilities rather than network proxies [3].

Mitigation code that actually holds

A defensible renderer requires layered controls at multiple levels:

  • Per-request interception that denies file:// and data: protocols before any fetch begins
  • Network-layer rejection of loopback, link-local, RFC1918, fe80::/10, and metadata hostnames
  • Canonicalized IPv4 literals to catch hex, octal, and integer encodings of blocked ranges
  • DNS pinning: resolve once at intercept time, cache the refusal, and pin the IP for any permitted fetch; never cache approvals
  • Egress allowlists in network namespaces that default-deny and only permit required outbound targets
  • IMDSv2 hop limit 1 on all instance metadata endpoints [1]
  • Logging of every blocked request for monitoring and incident response

The browser's request interceptor can be bypassed by JavaScript that does not trigger it. Network namespaces can be misconfigured. DNS pinning fails if your resolver is attacker-controlled. Each layer catches cases the others miss.

When to move rendering outside your trust boundary

SSRF is now part of OWASP's #1 category, Broken Access Control, after CWE-918 was folded into the Top 10:2025 [4]. The 2026 security literature documents a wave of SSRF specifically in PDF and HTML rendering pipelines.

When your application must render untrusted HTML, the cleanest architectural fix is to move rendering out of your VPC entirely. A hosted, isolated renderer keeps your secrets, databases, and metadata endpoints on your side of the wire while render requests go to an external service with its own egress restrictions and no access to your internal network.

MarkupGo runs rendering on isolated infrastructure with controlled egress, so your VPC never loads attacker-controlled HTML. Your API key authenticates the request, but the browser that executes the render cannot reach your database, your cloud metadata, or your internal services.

The next concrete step is to audit your rendering pipeline for the gaps above: check whether your headless browser can reach 169.254.169.254, whether it follows redirects without validation, and whether a file:// iframe in injected HTML can read your environment files.

Sources

  1. koltesecurity.com
  2. Headless browsers (bblabs.es)
  3. The ultimate Bug Bounty guide to exploiting SSRF vulnerabilities (yeswehack.com)
  4. OWASP Top Ten 2026 (securityboulevard.com)