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.
What we did at PDF4.dev
PDF4.dev runs a singleton Playwright Chromium per worker, recycled every N renders, behind a Postgres-backed queue. When CVE-2026-5287 landed on March 31, 2026, the response was three steps. First, we checked the deployed Chromium with await browser.version() against our internal floor. Second, we bumped Playwright to the patched line and rebuilt the Docker image. Third, we redeployed every worker, because a singleton browser holds the old binary in memory for its full lifetime, even after the package on disk is updated.
The warm-pool architecture is fast in steady state but has one subtle property: a vulnerable Chromium persists in RAM until the worker process is restarted. Re-deploying the package version is not enough. Every CVE response now ends with a forced rolling restart, even when the Dockerfile already pulled the patched binary on build.
We also wired Chrome Releases and the Playwright release feed into our PagerDuty so a stable channel update with a high-severity CVE pages the on-call. Internally, our Renovate config groups the four headless-Chromium packages (@playwright/test, puppeteer, puppeteer-core, @sparticuz/chromium) into a single high-priority PR so a patch reaches every service in one merge. The bundled Chromium for every PDF4.dev render is verified at boot against a hardcoded minimum, and a worker that fails the check refuses to take traffic.
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 want to move PDF rendering off your infrastructure entirely, PDF4.dev handles the Chromium upgrade window, sandbox isolation, and warm-pool recycling for you. The same engineering that closed CVE-2026-5287 within hours of release runs across every render on the platform.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



