CVE-2026-5287 is a high-severity use-after-free in Chromium's PDF rendering engine. Google patched it in Chrome 146.0.7680.178 on March 31, 2026. Any backend running Puppeteer, Playwright, headless Chrome, Electron, or a custom Chromium pipeline is exposed if it has not rolled a fresh Chromium since that date. This guide is the incident-response playbook: identify the vulnerable surface, verify the version, patch each stack, and harden the pipeline so the next CVE costs less.
What CVE-2026-5287 actually is
CVE-2026-5287 is a use-after-free in PDF in Google Chrome prior to 146.0.7680.178 that lets a remote attacker execute arbitrary code inside the Chromium renderer sandbox via a crafted PDF file. NVD scores it CVSS 8.8 (high) on vector AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, classified as CWE-416. The bug lives in the PDF rendering path used by both the in-browser viewer and any embed that triggers PDFium parsing. Attack requires user interaction (loading the crafted PDF), but server-side headless pipelines do that automatically on every request.
The patch shipped on March 31, 2026 in the desktop stable channel as Chrome 146.0.7680.178 for Windows and macOS, and 146.0.7680.177 for Linux. Microsoft Edge ingests the same Chromium tree, so the Edge fix landed the same week through the standard ingestion. The advisory listed two adjacent use-after-free bugs in the same release, CVE-2026-5286 (Dawn) and CVE-2026-5290 (Compositing), and a third high-severity zero-day, CVE-2026-5281, that was already being exploited in the wild against Chrome's WebGPU layer.
If your production PDF pipeline has not redeployed since March 31, 2026, treat this as a paged incident. The bundled Chromium does not auto-update inside a pinned Puppeteer or Playwright install: it ships exactly what was on disk when the image was built.
Are you affected? A checklist by stack
Most server-side PDF stacks pin a Chromium build at install time and never roll it forward without a deploy. Walk through your runtime and check each one. The fix landed in Chromium 146.0.7680.178, so the binding question for every layer is whether the bundled or installed Chromium is at or above that version.
| Stack | Vulnerable if Chromium is | Patched starting at |
|---|---|---|
| Puppeteer | bundled Chromium below 146.0.7680.177 (Linux) | puppeteer-core rolls landing after April 1, 2026 (verify in puppeteer changelog) |
| Playwright | 1.58.x ships Chrome for Testing 145.0.7632.6, vulnerable | 1.59.x ships Chromium 147.0.7727.15 (released April 2026), patched |
| Headless Chrome (system) | system Chromium below 146.0.7680.177 | apt/yum repo channels updated after March 31, 2026 |
| Electron | upstream Chromium below 146.0.7680.177 | check Electron release notes for the corresponding stable bump |
Docker images (browserless/chrome, node:20-bullseye + manual install) | image was built before April 1, 2026 | rebuild with apt upgrade or pull a freshly tagged image |
AWS Lambda Chromium layers (@sparticuz/chromium) | layer published before April 1, 2026 | bump to a release that bundles Chromium 146.0.7680.178 or later |
The non-obvious cases are the dangerous ones. A monorepo can pin Puppeteer in one service and Playwright in another. A Lambda layer compiled into a deployment artifact months ago is still serving traffic. A Dockerfile that runs apt-get install chromium once at build time freezes the version forever, and docker pull only refreshes if the tag floats.
How to verify your Chromium version
Run the matching command for each tool you ship. Output the exact version string and compare it against 146.0.7680.177 for Linux, 146.0.7680.178 for Windows and macOS. Anything below those numbers is exposed to CVE-2026-5287 and should be patched in the same change window.
For Puppeteer:
# Show the Chromium build that the current install is using.
npx puppeteer browsers list
# Or, from inside a running container:
node -e "console.log(require('puppeteer').executablePath())" \
| xargs -I{} {} --versionFor Playwright:
# Show the bundled Chromium version.
npx playwright --version
cat node_modules/playwright-core/browsers.json | jq '.browsers[] | select(.name=="chromium")'For a system Chromium or chromium-browser package (Docker images, Linux servers):
chromium --version
# Or whatever binary the image installs.
google-chrome --versionFor Electron:
node -e "console.log(process.versions)"For an AWS Lambda layer using @sparticuz/chromium or a similar binary:
# Inside the Lambda, log the version on cold start.
const chromium = require("@sparticuz/chromium");
const browser = await puppeteer.launch({ executablePath: await chromium.executablePath() });
console.log(await browser.version());The output you want to see is HeadlessChrome/146.0.7680.177 or higher. If it reads 145.x.x or below, you have not patched.
The patch path: upgrading each stack
Patching is straightforward once you know the stack. The hard part is shipping it through every environment that runs Chromium, including dev images, CI runners, and any third-party container that wraps your code. Roll the upgrade per layer, not per dependency, so a single forgotten Dockerfile does not leave a vulnerable replica running.
Puppeteer
Bump puppeteer (or puppeteer-core) to a release that pins a Chromium at or above 146.0.7680.177. Check the changelog at pptr.dev/CHANGELOG and find the first version that lists "Roll Chrome to 146.0.7680.x" with x ≥ 177. Versions in the 24.39.x and 24.40.x lines were rolling through 146.0.7680 patch numbers in March 2026, so a fresh 24.40.0 or later is the right floor (verify in puppeteer changelog).
npm install puppeteer@latest
# Or, for a pinned executable:
npm install puppeteer-core@latest
npx puppeteer browsers install chromeIf you set PUPPETEER_SKIP_DOWNLOAD=true and supply your own Chrome, update that Chrome instead. The Puppeteer package version on its own does not protect you when executablePath points at an external binary.
Playwright
Upgrade to Playwright 1.59.0 or later, which bundles Chromium 147.0.7727.15. The previous 1.58.x line ships Chrome for Testing 145.0.7632.6, which is below the patch threshold. Playwright rolled the Chromium dependency on the main branch shortly after Chrome 146.0.7680.31 landed and shipped it broadly with the 1.59 release in April 2026.
npm install -D @playwright/test@latest
npx playwright install chromiumAfter the install, re-run npx playwright --version and grep for the Chromium version in browsers.json. CI caches at ~/.cache/ms-playwright (Linux) or the Windows equivalent must also be invalidated; otherwise the runner reuses the old binary.
Docker images
Two patterns cover almost every Docker setup. The first is a base image built with apt-get install chromium, where the install pins the version at build time. The second is a Puppeteer or Playwright base image (mcr.microsoft.com/playwright, browserless/chrome) that ships a pre-bundled Chromium.
For the first, rebuild from scratch with the latest apt index:
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y \
chromium \
&& rm -rf /var/lib/apt/lists/*docker build --no-cache -t my-pdf-service .
docker run --rm my-pdf-service chromium --versionFor the second, repin the upstream tag and pull:
docker pull mcr.microsoft.com/playwright:v1.59.0-jammy
docker pull ghcr.io/browserless/chromium:latestFloating tags like latest are a cache hazard in CI: the build agent may keep a stale copy. Always combine docker pull with docker build --no-cache for security-critical rebuilds.
AWS Lambda layers
Lambda Chromium layers are the worst case because they are baked into a deployment artifact and never auto-update. The @sparticuz/chromium package is the most popular layer; bump it to the release that includes Chromium 146.0.7680.177 or later, then redeploy every Lambda that consumes it.
npm install @sparticuz/chromium@latest
# Then sam build && sam deploy, or your IaC equivalent.If you publish your own layer, rebuild it from a fresh Chromium download and bump the layer version in your IaC. Forgetting to bump means every existing Lambda keeps using the old layer until it is explicitly updated.
Electron
Electron pins a Chromium upstream per release. Find the Electron version that targets Chromium 146.0.7680.177 or later in the Electron release notes, then upgrade the desktop app and force users to update through your existing channel (auto-update, signed installer, etc.). Pre-installed Electron apps cannot be hotfixed remotely without an update path.
Hardening for the next CVE
A patch is a fire drill; a pipeline that survives the next one is the goal. Three practices make Chromium CVEs cheap to handle: a hard floor on the bundled version, automated dependency PRs, and a clear rebuild signal that does not depend on humans noticing a blog post. Pick all three.
Pin a minimum Chromium version in CI, not just in package.json. Add a script that runs on every build and fails if the bundled Chromium is below your floor. The check is one shell command:
# Fails the build if Chromium is below 146.0.7680.177.
VERSION=$(npx puppeteer browsers list | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
node -e "
const v = '$VERSION'.split('.').map(Number);
const min = [146,7680,177];
// Compare major.build.patch (skip the second segment which moves with branch cuts).
if (v[0] < min[0] || (v[0]===min[0] && (v[2] < min[1] || (v[2]===min[1] && v[3] < min[2])))) {
console.error('Chromium below CVE-2026-5287 floor:', '$VERSION'); process.exit(1);
}
"Automate the dependency PRs. Renovate's preset :dependencyDashboard plus a custom rule on puppeteer, playwright, @sparticuz/chromium, and electron opens a PR within hours of every upstream release. Dependabot does the same, with weaker scheduling controls. Either is better than manual npm outdated checks.
{
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackageNames": ["puppeteer", "puppeteer-core", "playwright", "@playwright/test", "@sparticuz/chromium", "electron"],
"groupName": "headless chromium",
"schedule": ["before 9am every weekday"],
"prPriority": 5,
"labels": ["security", "chromium"]
}
]
}Subscribe to the right feeds. The Chrome Releases blog is the official channel for stable updates and CVE disclosures. The Puppeteer and Playwright GitHub release feeds are the fastest signal that a patched Chromium is in your tooling. The Chromium security mailing list announces the underlying issue. Wire all three into your team's incident channel.
Sandbox isolation matters even when Chromium ships a CVE every month. Run the headless browser as a non-root user, drop Linux capabilities, and keep --no-sandbox off. The renderer sandbox is what limits CVE-2026-5287 to "code execution inside a sandbox" rather than full host RCE; disabling it removes the only barrier between a malicious PDF and your filesystem.
Why headless PDF pipelines are an outsized attack surface
A headless Chromium pipeline parses untrusted input on every render. Most threat models assume the browser is the user's; a server-side pipeline flips that assumption. The attacker can choose the input, retry the exploit at scale, and study failures without burning a victim. That makes any PDF parsing CVE in Chromium materially worse for a backend than for a desktop user.
Three things compound the risk. First, server-side renders happen continuously, so a single vulnerable replica can serve thousands of malicious payloads per minute. Second, the same renderer process is reused across requests in many implementations (singleton browser, page pool), so memory corruption persists past a single render. Third, the output (a PDF) is often delivered back to the attacker as a download URL, giving them a side channel for whatever the exploit reads from memory.
PDF rendering specifically is high-risk because PDFium is one of Chromium's largest C++ surfaces. The format is complex: streams, fonts, JavaScript actions, embedded images, encryption, forms. Every parser path is a potential use-after-free. CVE-2026-5287 is the latest in a long sequence; CVE-2026-5894 (PDF navigation bypass, fixed in 147.0.7727.55 on April 8, 2026) and CVE-2026-6305 (a PDFium heap overflow disclosed later in 2026) are reminders that the fix tempo will not slow down.
The defensive lesson is to constrain what the renderer can do per render: short-lived browser contexts (browser.newContext() per request rather than a long-lived page), strict CSP on the rendered HTML, no --allow-file-access-from-files, no shared writable disk between renderer and host. None of these stop a use-after-free, but each one limits the blast radius if a future CVE escapes the sandbox.
How PDF4.dev handles a Chromium CVE
PDF4.dev runs one Playwright Chromium per server process, shared across renders and closed after five minutes without an active page. That shape has a direct consequence for a CVE like this one: installing a patched Playwright package does not patch the browser already running in memory. The fix only takes effect when the process restarts. On PDF4.dev a Railway redeploy restarts the container, so bumping Playwright and redeploying is the response. The five-minute idle teardown does not help on its own, because it relaunches the same binary that is on disk.
A long-lived browser keeps its binary for its whole lifetime. If you run a warm browser, a patch reaches production when the process restarts, not when the package installs. Make the restart part of the patch.
If you run headless Chromium yourself, three habits turn the next CVE into routine work:
- Follow the Chrome Releases blog and the Playwright release notes, so a high-severity stable update reaches you the day it ships.
- Group every headless Chromium package (Playwright, Puppeteer,
@sparticuz/chromium) under one dependency-update rule, so a single merge patches all of them. - Check
await browser.version()at startup against a minimum you maintain, and refuse to serve traffic when it is older.
The takeaway for any team running headless Chromium: a CVE in Chrome is not a desktop user's problem if it is in your dependency graph. Treat the bundled Chromium as production code, pin a floor, automate the bump, and force a process restart on every patch. The next high-severity Chromium PDF CVE is already being researched; the goal is to make the response routine rather than urgent.
If you would rather not own that patch cycle, PDF4.dev runs the Chromium and applies the upgrades, so your application only makes an HTTP request.
Free tools mentioned:
Frequently asked questions
- What is CVE-2026-5287?
- CVE-2026-5287 is a high-severity use-after-free vulnerability in Chromium's PDF rendering pipeline. A crafted PDF file can trigger arbitrary code execution inside the Chromium renderer sandbox in any Chrome build below `146.0.7680.178`. Google released the fix on March 31, 2026 in the desktop stable channel.
- Is my Puppeteer or Playwright pipeline affected?
- Yes, if it ships a pinned Chromium older than `146.0.7680.178`. Puppeteer and Playwright both bundle their own Chromium build, so a backend that has not bumped the package since March 2026 is almost certainly running a vulnerable version. Run `npx puppeteer browsers list` or check `node_modules/playwright-core/browsers.json` to verify.
- Can the bug be triggered by a PDF generated server-side?
- It can be triggered by any input the headless browser parses as a PDF. Server-side pipelines that ingest user-uploaded PDFs, embed third-party PDFs, or proxy attacker-influenced URLs into Chromium are exposed. Pure HTML-to-PDF flows that never load a remote PDF are lower risk but still exposed if Chromium's internal PDF viewer is invoked anywhere in the request path.
- Does Microsoft Edge need a separate patch?
- No. Microsoft Edge ingests Chromium upstream and inherits the fix. The April 2 advisory from Microsoft confirms that Edge picks up CVE-2026-5287 through the standard Chromium roll. Update to the matching Edge build released the week of March 31, 2026.
- How fast did exploitation appear in the wild?
- CVE-2026-5287 itself has not been confirmed as exploited in the wild at the time of writing. The same March 31, 2026 patch shipped CVE-2026-5281, a use-after-free in Dawn (Chromium's WebGPU layer) that Google flagged as actively exploited and CISA added to the Known Exploited Vulnerabilities catalog on April 1, 2026. Attackers tend to investigate adjacent CVEs in the same release.
- What is the minimum Chromium version I should pin?
- `146.0.7680.178` for Windows and macOS, `146.0.7680.177` for Linux. Most server-side PDF pipelines run Linux, so `146.0.7680.177` is the practical floor. If you can move to Chromium 147 (the current Playwright 1.59 baseline), do so, since CVE-2026-5894 (a separate PDF navigation bypass) was fixed in `147.0.7727.55` on April 8, 2026.
- How can I get notified about the next Chromium PDF CVE?
- Subscribe to the Chrome Releases blog stable channel feed, the Chromium security RSS, and the GitHub release notifications for `puppeteer/puppeteer` and `microsoft/playwright`. Pair these with Renovate or Dependabot rules that auto-PR Chromium-related package bumps. Most teams find out about CVEs faster from the Puppeteer or Playwright changelog than from MITRE.
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



