Google patched two actively exploited V8 zero-days in Chrome within six days of each other in September 2026: CVE-2026-85046 on September 3 and CVE-2026-87491 on September 8. Both are described the same way in Google's advisory language, "execute arbitrary code inside the sandbox via a crafted HTML page". If your product turns HTML into PDF on a server, that sentence is a description of your normal workload.
What actually shipped in September 2026
Two V8 bugs, both exploited in the wild before the fix, both reachable from a single crafted HTML page. CVE-2026-85046 is a type confusion in V8 scored CVSS 8.8, fixed in Chrome 152.0.7977.82/.83 for Windows and macOS and 152.0.7977.82 for Linux. CVE-2026-87491 is an out-of-bounds write in V8, fixed in Chrome 153.0.8010.36/.37 and 153.0.8010.36 for Linux.
CVE-2026-85046 was reported by Salvatore Gulizia on August 4, 2026, and shipped in a stable release carrying 12 security fixes. CVE-2026-87491 came from Jihyeon Jeong of Compsec Lab at Seoul National University, reported August 6, 2026, and landed in the Chrome 153 stable release that carried 230 security fixes. Google's Chromium team rated CVE-2026-87491 medium severity; NVD-derived trackers list it as CVSS 8.8 high on vector AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. The UI:R in that vector means user interaction is required. On a desktop, that is a person visiting a page. On a PDF worker, that is your API accepting a request.
CISA added both to the Known Exploited Vulnerabilities catalog within a day of each patch, under Binding Operational Directive 26-04.
| CVE | Component | Chrome fix | Shipped | KEV added | FCEB due |
|---|---|---|---|---|---|
| CVE-2026-85046 | V8 type confusion | 152.0.7977.82/.83 | Sep 3, 2026 | Sep 4, 2026 | Sep 18, 2026 |
| CVE-2026-87491 | V8 out-of-bounds write | 153.0.8010.36/.37 | Sep 8, 2026 | Sep 9, 2026 | Sep 23, 2026 |
Why V8 dominates the 2026 zero-day list
Four of the seven Chrome zero-days exploited in the wild in 2026 are V8 bugs. V8 is the JavaScript and WebAssembly engine, and its optimizing compilers do speculative type work that is exactly where type confusion and out-of-bounds primitives come from. A researcher who lands a type confusion gets an arbitrary read and write on the JavaScript heap, which is the standard first stage of a renderer exploit.
Here is the full 2026 sequence, per Security Affairs:
| CVE | Month | Component |
|---|---|---|
| CVE-2026-2441 | February | Use-after-free in CSS |
| CVE-2026-3909 | March | Out-of-bounds write in Skia |
| CVE-2026-3910 | March | V8 |
| CVE-2026-5281 | April | Use-after-free in Dawn (WebGPU) |
| CVE-2026-11645 | June | Out-of-bounds memory access in V8 |
| CVE-2026-85046 | September | Type confusion in V8 |
| CVE-2026-87491 | September | Out-of-bounds write in V8 |
The pattern matters more than any single entry. Seven exploited zero-days in roughly nine months is a cadence of one every five to six weeks. Planning around "we will patch when there is a CVE" produces an emergency every month and a half. Planning around "the renderer will be compromised eventually" produces a design.
The thesis: an HTML-to-PDF service is a browser running untrusted code
An HTML-to-PDF API is a browser with an HTTP front door. The request body is the URL bar. Playwright's page.setContent() hands markup to a real Chromium renderer, which parses the HTML, builds the DOM, resolves CSS, decodes images through Skia and runs script through V8. Nothing about the rendering path is a reduced or sanitized browser. It is the browser.
The differences from a desktop browser all cut against the server. A desktop user visits a hostile page occasionally and by accident. A PDF worker accepts hostile input by design, on every request, at whatever rate your API allows. A desktop Chrome updates itself silently within days. A PDF worker runs whatever Chromium your container image baked in, for as long as that image is deployed. A desktop renderer holds one user's browsing session. A pooled PDF renderer may hold fragments of several tenants' documents in the same address space.
That last point is the one worth internalizing. A renderer exploit on a PDF worker does not need to escape the sandbox to be valuable. Everything the attacker wants, the document being rendered, the remains of prior renders in unreclaimed heap, the template source, is already inside the process the exploit controls.
The common objection is that templates are authored by trusted customers, not by attackers. That holds only until the first template interpolates a value someone else controls. An invoice template that renders a customer-supplied company name, a report that embeds a remote image URL, a certificate that prints a form field: each of those is a path from an untrusted string into the markup Chromium parses. The trust boundary sits at the data, not at the template, and in most products the data comes from the open internet.
Measuring the bundled Chromium lag
Playwright and Puppeteer pin a Chromium build per release, so there is always a window between Google shipping a fix and that fix reaching your pipeline. Measure the window by comparing three numbers: the patched Chrome version, the Chromium version your package bundles today, and the date the roll landed upstream.
As of September 22, 2026, Playwright 1.63 bundles Chromium 153.0.8010.12 per the Playwright release notes. The floor that covers CVE-2026-87491 is 153.0.8010.36. That is below the floor. Puppeteer rolled to Chrome 153.0.8010.47 in PR #15465, merged September 17, 2026, nine days after Google's stable release.
Check the version your process will actually launch, not the npm package version:
# Playwright: print the binary path, then ask the binary itself.
node -e "console.log(require('playwright').chromium.executablePath())"
"$(node -e "console.log(require('playwright').chromium.executablePath())")" --version
# Puppeteer: list every Chromium build on disk.
npx puppeteer browsers list
node -e "console.log(require('puppeteer').executablePath())" | xargs -I{} {} --version
# chrome-headless-shell, if that is what you launch.
chrome-headless-shell --versionTurn the floor into a boot check so a stale image cannot serve traffic quietly:
// Refuse to start if the launched Chromium is below the known-patched floor.
import { execFileSync } from "node:child_process";
import { chromium } from "playwright";
const FLOOR = [153, 0, 8010, 36];
function parse(version: string): number[] {
const match = version.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if (!match) throw new Error(`unparsable Chromium version: ${version}`);
return match.slice(1).map(Number);
}
const raw = execFileSync(chromium.executablePath(), ["--version"]).toString();
const actual = parse(raw);
for (let i = 0; i < FLOOR.length; i++) {
if (actual[i] > FLOOR[i]) break;
if (actual[i] < FLOOR[i]) {
throw new Error(`Chromium ${actual.join(".")} is below the floor ${FLOOR.join(".")}`);
}
}A worker that refuses traffic is loud. A worker running an eight-week-old Chromium is silent, and silence is how patch lag becomes a breach.
Isolation: what to do when the patch is not there yet
The patch window is never zero, so the design question is what an exploited renderer can reach. Six controls, in rough order of how much they reduce blast radius per unit of effort.
Keep the sandbox on. Do not pass --no-sandbox. It is the most copied line in headless Chromium Dockerfiles and it removes the boundary that keeps both September V8 bugs at "code execution inside the sandbox". If Chromium fails to start in your container, the fix is a seccomp profile and a non-root user, not the flag.
Run as a non-root user. Create a dedicated user in the image, own only the cache and temp directories, and mount everything else read-only. A renderer compromise then starts from an account that cannot write to the application code.
One process per render, or per small N. A long-lived singleton browser keeps heap state across tenants. Recycling the browser context every render, and the browser process every N renders, bounds how much of someone else's document is still resident when an exploit fires. The cold-start cost on headless Chromium is small compared to what it buys.
Deny internal egress. The renderer should reach your font host and your asset CDN, and nothing else. Block RFC 1918 ranges and the cloud metadata endpoint at 169.254.169.254 at the network policy layer, not in application code. An exploit that cannot reach the metadata service cannot trade renderer access for cloud credentials.
Block file:// and local schemes. Strip file://, filesystem:// and chrome:// URLs from incoming HTML before it reaches setContent, and run with a request interceptor that drops them at the network layer as a second line. Local file reads are the cheapest exfiltration path in a PDF renderer.
Hard timeouts on every render. A per-render wall clock, enforced by killing the process rather than awaiting a promise, stops a hung or looping page from pinning a worker and gives you an upper bound on how long a malicious page gets to work.
How PDF4.dev sits in this threat model
PDF4.dev runs user-supplied HTML through headless Chromium. That is the product, so the attack surface described above is ours too, and claiming otherwise would be dishonest. What a managed service changes is who owns the patch window and the isolation defaults, not whether the class of bug applies.
Concretely, that means a Chromium version floor enforced at worker boot, renderers running as a non-root user with the sandbox enabled, contexts recycled between renders, egress restricted to the asset and font paths, and a hard timeout per render. When Google ships an out-of-band stable update for an exploited zero-day, the upgrade is a fleet operation on our side rather than a dependency bump, a CI cache invalidation and a redeploy on yours.
The honest framing: a managed renderer shortens the exposure window and standardizes the isolation, and there is still a window. Anyone selling HTML-to-PDF who tells you their renderer is immune to V8 bugs is describing a browser that does not exist.
Takeaway
Seven exploited Chrome zero-days in nine months, four of them in V8, is a stable rate rather than a bad year. The operational conclusions follow from the rate, not from any one CVE: know which Chromium version your workers launch, enforce a floor at boot, keep the sandbox on, recycle renderer processes, and close off the file system and the internal network before the next advisory lands.
The next V8 zero-day is already being written. The pipelines that absorb it without an incident are the ones that assumed the renderer would be compromised and designed for what happens after.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



