Two Critical Next.js RCEs Shipped the Same Day: What I Actually Checked Before Patching My Server
On August 25, 2026, Next.js shipped 15.5.24 and 16.3.3 to patch two unrelated critical RCEs at once. I run this blog's CMS as a self-hosted Next 15 app behind nginx, PM2, and Cloudflare, so nobody patched it for me. Here's the exposure check I ran before touching npm install, the actual patch commands, and why "not currently exposed" still wasn't a reason to skip the upgrade.

On this page
On August 25, 2026, Next.js shipped 15.5.24 and 16.3.3 to patch two critical-severity RCEs at once — one with CVSS 9.0, the other 9.5. I run this blog's CMS on a self-hosted Next 15 build behind nginx, PM2, and Cloudflare, which means nobody patched it for me. So before touching npm install, I actually checked what was exposed: the OS-level bug didn't apply to my Linux server, and the image-pipeline bug required a config flag I'd never set. Here's the exposure check, the patch, and why "not currently exposed" still isn't a reason to skip the upgrade.
What shipped, and why the timeline moved
Vercel announced the August security release on August 20, targeting a single critical vulnerability for August 26. On the morning of the 25th, they moved the release forward by a day after finding a second critical issue in an upstream dependency, and shipped both fixes in the same release so nobody would have to upgrade twice. The full advisory landed later that day.
Two separate GitHub Security Advisories, two separate root causes:
GHSA-p293-qw3h-jr36 / CVE-2026-75604 — unauthenticated RCE via path traversal (CWE-22), CVSS 9.0, on apps that use both the Pages Router and App Router without Cache Components, when the server runs on a Windows filesystem. Affected: >=13.4 <15.5.24 and >=16.0 <16.3.3. No known workaround — Windows-hosted deployments had to upgrade, full stop.
GHSA-2xp9-vwfh-vxw4 — unauthenticated RCE via the built-in Image Optimization API, CVSS 9.5, when Next.js processes an attacker-controlled AVIF file. The bug actually lives in libheif, the library sharp calls to decode AVIF, tracked upstream as GHSA-g89c-p67h-r497. Affected: >=10.0.0 <15.5.24 and <16.3.3 — effectively every Next.js release with image optimization turned on, going back six years. The patched build disables AVIF optimization outright until libheif ships its own fix.
The libheif advisory is worth a paragraph because it explains why an image-decoding bug reaches "critical RCE" instead of "crash." libheif's scale_nearest_neighbor() allocates its destination buffer using the bit depth of the first alpha channel it finds on an image — but a crafted AVIF file can chain an iden (identity-derivation) item that pulls in a second alpha plane at a higher bit depth than the first. The scaler still allocates for 8-bit samples, then iterates into the second, wider plane and casts the buffer to uint16_t* anyway, writing two bytes per sample into a buffer sized for one. In the advisory's own worked example, that's a 128×128 buffer allocated at 16,384 bytes receiving 32,768 bytes of writes — roughly 16KB spilling past the end of the heap allocation, with both the overflow size and the bytes written attacker-controlled through the container structure and bitstream content. A heap overflow of that shape, in a native image-decoding library invoked from a Node process handling untrusted uploads, is exactly the pattern that turns into remote code execution rather than a segfault.
Same day, same release, two unrelated bugs with a decade of combined version range between them. That's the kind of release that makes you stop scrolling and actually go check your own server.
Vercel-hosted vs. self-hosted is the line that actually matters
Vercel's own changelog for this release says Vercel-hosted applications needed to do nothing: AVIF optimization was disabled across their managed Image Optimization service, and their infrastructure runs on Linux, so the Windows bug never applied. Self-hosted deployments got a different instruction — upgrade to 15.5.24 or 16.3.3 yourself, on your own timeline, with nobody doing it for you.
This blog is the second kind. It's a Next 15 app built with output: 'standalone', running as a PM2-managed Node process behind nginx, with Cloudflare in front for DNS, TLS, and CDN caching. That's a deliberately un-Vercel setup — I like owning the box — and it's also the setup I build for client projects that need a managed deployment rather than a platform lock-in. The tradeoff is explicit: I don't get automatic protection when Vercel patches its edge. I get an email-shaped advisory and an obligation to act on it myself.
Checking exposure before patching
Two questions before I touched anything: does this server run on the filesystem the Windows bug needs, and is AVIF optimization actually turned on.
The first one is trivial:
$ uname -a
Linux beru-vps 6.8.0-x86_64 #1 SMP x86_64 GNU/Linux
Linux. GHSA-p293-qw3h-jr36 explicitly states Linux and macOS aren't affected — the path-traversal primitive only becomes exploitable against a Windows filesystem's path-separator handling. That bug was never going to touch this server regardless of version.
The AVIF bug needed more digging, because "is optimization affected" isn't a yes/no on the framework version alone — it depends on whether images.formats in next.config.js includes image/avif. I hadn't set it, but "I don't remember doing that" isn't verification, so I went and read the actual default out of the installed package rather than trust my own memory:
$ grep -n "formats" node_modules/next/dist/shared/lib/image-config.js
57: formats: [
58: 'image/webp'
59: ],
Default formats is ['image/webp'] only. AVIF is opt-in, not opt-out — you have to explicitly list image/avif in your image config for Next.js to ever call the AVIF transformer. I confirmed the other side of that too, checking where the transformer actually gets invoked:
$ grep -n "avif" node_modules/next/dist/server/image-optimizer.js
12:const AVIF = 'image/avif';
88: transformer.avif({
transformer.avif() is sharp's AVIF encoder — the exact call path the advisory names as the exposure vector — and it's only reachable if a request negotiates an AVIF response, which only happens if the format is in that config array. My next.config.js never added it. So on the specific bug shipped this week, this CMS wasn't actively exposed to either vulnerability: wrong OS for one, opted out of the format for the other.
Patching anyway
"Not currently exposed" is a statement about today's config on today's version. It says nothing about six months from now, when I add a feature, forget this post, and toggle images.formats to squeeze out smaller image payloads — a completely reasonable thing to want, and exactly the moment a dormant CVE stops being dormant. Version pinning to a build with a known critical RCE in it, on the theory that current settings happen to route around it, is the kind of decision that looks fine right up until it doesn't. So: patch regardless of exposure.
$ npm view next dist-tags
{
latest: '16.3.3',
backport: '15.5.24',
canary: '16.4.0-canary.10',
...
}
backport is the tag Vercel uses for security fixes landed on the older major that's still under maintenance — 15.5.24 is exactly that, a patch on the 15.x line rather than a forced major upgrade to 16. That mattered here: I didn't want a framework major-version bump bundled into an emergency security patch, and I didn't have to take one.
$ npm install [email protected]
$ npm run build
$ pm2 reload beru-cms --update-env
npm run build with output: 'standalone' regenerates .next/standalone, which is the directory PM2 is actually pointed at — skipping the rebuild and just bumping the dependency in package.json would have left the running process on the old bundled code. pm2 reload (not restart) does a graceful reload for a process managed under cluster mode, so nginx doesn't see a connection gap mid-deploy. Verification after:
$ npm ls next
[email protected]
└── [email protected]
Total time from advisory to verified patch on a single-app VPS: about fifteen minutes, most of it the build step. That's the actual argument for standalone self-hosting over more elaborate container pipelines when you're a team of one — there's no image registry push, no orchestrator rollout, no staging cluster to march the change through. npm install, rebuild, reload.
I deliberately didn't try to reproduce the exploit itself against production — throwing a malformed AVIF payload at a live box to "confirm" a heap overflow is gone is not a test, it's a second incident waiting to happen, and the GHSA advisory's own version-range table is a more trustworthy source of truth than anything I'd learn from that. What I did verify, separately from npm ls next reporting the right dependency version, was that the specific line deciding exposure — images.formats in next.config.js — still had no avif entry after the rebuild, by re-reading the deployed config file itself rather than trusting that whatever I deployed matched what I intended to deploy. Small distinction, but it's the difference between "I patched" and "I patched and confirmed the config that made this bug irrelevant to me is still the config that's actually running."
Why 15.5, not 16
The official post is explicit that this release covers "v16.3.3 (Active LTS) and v15.5.24 (Maintenance LTS)" — two separate maintenance lines getting the same fix, not one canonical version everyone converges on. This CMS stayed on the 15.x line on purpose: 16 landed breaking changes to the caching model (Cache Components moved from experimental to part of the routing story) that I haven't budgeted time to test against this codebase, and a security release is the wrong moment to absorb an unrelated migration under time pressure. The backport dist-tag existing at all is what makes that a safe choice instead of a stubborn one — Vercel is committing to shipping security fixes on 15.x for as long as it's in maintenance, so staying there isn't the same as staying unpatched.
What Cloudflare in front doesn't buy you here
Cloudflare sits in front of nginx for DNS, TLS termination, and edge caching, and it's tempting to treat that as a layer of defense against exactly this kind of bug. It isn't, for either CVE. The Windows path-traversal issue is a property of how the Next.js server process resolves filesystem paths on the box it's running on — Cloudflare never sees the filesystem, so it can't shield it. The AVIF issue is a heap buffer overflow inside libheif's pixel-scaling code, triggered by the byte content of a file sharp was asked to decode — Cloudflare's WAF can rate-limit or block requests by pattern, but it isn't decoding AVIF bitstreams to check whether the alpha-channel byte layout is going to overflow a buffer three services downstream. A CDN in front of your app is a real mitigation for a lot of things — DDoS, some classes of injection, basic bot traffic — and it's not a substitute for patching the application server. I mention this mostly because I've caught myself, in less careful moments, treating "there's a CDN in front of it" as more of a security boundary than it actually is.
The boundary that kept this out of the data layer
Worth being precise about what this bug touches and what it doesn't, because "critical RCE in your framework" is the kind of headline that makes people reach for the wrong mitigation. Both vulnerabilities live in the image-optimization request path — /_next/image and the server code that resolves, transforms, and caches an image response. Neither touches routing logic outside that endpoint, neither touches the data layer. This CMS runs on Prisma against Postgres for every piece of actual content — posts, drafts, tags — and none of that code path intersects with sharp or libheif at all. An RCE is an RCE and I'm not going to undersell it: successful exploitation gets arbitrary code execution on the host, which is bad regardless of what else runs on that host afterward. But it's still worth tracing the actual blast radius of a specific CVE instead of reacting to the CVSS number alone — the fix here was scoped to one dependency in one request path, not a framework-wide rewrite, and the patch reflected that: a version bump, not a migration.
A short list for anyone else self-hosting Next
If you're running Next.js outside Vercel — and a fair number of agencies here do, mostly on cost grounds, on a VPS behind nginx and PM2 the same way this blog is — the checklist from this release is short and repeatable for the next one:
Check your OS first; it rules out an entire vulnerability class in one command. Check next.config.js for images.formats before assuming a bug about image optimization doesn't apply to you — "I don't use that feature" is a claim worth verifying against the actual config file, not your memory of writing it. Confirm the dependency chain a given CVE runs through (sharp, libheif, whatever it names) so you know whether the fix is in Next.js itself or an upstream library Next.js bundles. And patch on the maintenance line you're already on — the backport npm dist-tag exists so a security fix doesn't force a major-version migration onto an unrelated Friday afternoon.
None of that is specific to this CVE. It's the same checklist I run for production sites I keep patched for other people, and it's the same reason this blog's own OAuth-protected MCP server and its llms.txt agent-discovery setup run on the same box I just patched — the framework serving human readers and the framework serving agents are, in this deployment, the exact same process, so a critical RCE in the app server is never a problem I get to scope down to "just the blog part."
Where this actually leaves things
Two critical RCEs, one release, fifteen minutes to patch a single-app deployment once I'd done the exposure check properly. Neither bug turned out to be live against this specific server — wrong OS for one, unset config flag for the other — and I patched both anyway, because the version range being vulnerable doesn't stop being true just because today's settings happen to route around it. The useful habit isn't "I read the advisory," it's reading the advisory closely enough to know exactly which line in which config file decides whether it applies to you, and then checking that line instead of trusting what you remember writing six months ago.
Written and tested by Nguyen Hoang Loc (Beru), Ho Chi Minh City
