5 min read

Next.js 16.3.3 Fixes Two Critical RCEs. The Patch Drill I Ran

Last week my patch backlog jumped the queue

I had a quiet Tuesday evening planned: fix one flaky test, close the laptop. Then a friend sent me a link with "CVSS 9.5" and "unauthenticated" in the same sentence, and I spent the night patching two Next.js apps instead. If you self-host anything built on Next.js, you probably did something similar. Vercel shipped fixes for two critical remote code execution bugs in the same release, and the details are worth understanding before you type npm install and move on.

The short version: update to 16.3.3 on the 16.x line or 15.5.24 on the 15.x line. Everything below is the why and the exact steps I ran, because the obvious check (glancing at your package.json) is exactly the one that lies to you.

Two bugs, one release

The first one, GHSA-2xp9-vwfh-vxw4, lives in the image optimization pipeline. The second, CVE-2026-75604, is a path traversal that only bites Windows-hosted servers. Both can end in unauthenticated remote code execution. Both are fixed in 16.3.3 and 15.5.24, but the affected ranges reach back a long way: 10.0.0 for the AVIF bug, 13.4 for the Windows one. If you have not patched since late August, assume you are in scope.

One mercy up front: if your app runs on Vercel, the platform absorbs both fixes and you need to do nothing. This post is about the rest of us, running Docker containers on some VPS at 2am.

The AVIF bug is three dependency hops deep

Here is the part that stuck with me. The bug is not in Next.js code. Next.js hands image optimization to sharp. sharp hands AVIF decoding to libheif, a C library. And libheif had a heap buffer overflow: a crafted AVIF file makes the decoder allocate a buffer for an 8-bit alpha plane, then write 16-bit values into it, overrunning the allocation by about 16,384 bytes. That is a big, controllable corruption primitive, and the researchers behind the advisory said they got RCE on multiple real applications with a Python proof of concept.

Three hops down your dependency tree, none of it visible in your lockfile as anything suspicious, and an attacker can reach it by pointing your image optimizer at a file they control. Every libheif release through 1.23.1 is affected. There is no libheif fix yet, so the Next.js patch does the blunt thing: it disables server-side AVIF optimization entirely until the upstream fix lands.

Now, the nuance that decides whether you were even exposed. The vulnerable path only runs if you asked for AVIF output in your config:

// next.config.js
images: {
  formats: ['image/avif', 'image/webp'],
}

The default formats list is webp only. If you never added 'image/avif', the decoder never touched untrusted files and this particular hole was closed before it opened. If you did add it, because some performance audit in 2024 told you AVIF would save 40% of your image bytes (it does), then you were in scope. My dashboard app was. Irony noted.

The Windows one has no workaround

CVE-2026-75604 is narrower but meaner. If your server runs on a Windows filesystem, your app uses both the Pages Router and the App Router, and you have not enabled Cache Components, encoded backslash sequences in route segments could escape the cache directory. Traversal past the cache root leaks private build data, including the server-reference-manifest encryption key. Leaking that key leads to remote code execution.

Linux and macOS deployments are untouched, which covers most Node hosting. But Windows Node hosting is more common than people admit: Azure App Service, enterprise IIS-fronted boxes, legacy Windows containers. For those, the advisory says there is no known workaround. No config flag, no proxy rule. Upgrading is the only fix, and a public PoC already exists.

The drill I ran

First, check the version that is actually installed, not the range in package.json:

node -e "console.log(require('next/package.json').version)"

A caret range like "next": "^16.2.0" will happily sit next to a lockfile pinning 16.3.2. The lockfile wins, so read the installed package. If you deploy in Docker, check inside the image, because that is where the answer matters:

docker exec myapp node -e "console.log(require('next/package.json').version)"

Then upgrade on your line, rebuild, redeploy:

npm install [email protected]   # or [email protected] on the 15.x line

Updating package.json alone does nothing until you rebuild the deployment artifact and restart every running instance. I keep these check-and-patch commands saved in Snippet Ark because when a 9.5 drops, nobody wants to reconstruct shell one-liners from memory. I also left the emergency rollback tag from the previous deploy standing by, which turned out to be unnecessary but cost nothing.

One side effect to expect: after the patch, AVIF requests come back unoptimized, full original size. If your Core Web Vitals dashboards start looking sad, that is the temporary trade-off, not a regression you caused. WebP still works.

What I actually took away

The AVIF bug is the best argument for supply-chain paranoia I have seen in a while, and it did not involve a compromised package at all. Nobody in my dependency tree did anything wrong except exist. A C library I did not know I depended on, two layers below the package I chose, became my attack surface because I let my server decode files that strangers could influence.

The general version of that lesson: any long-running server process that decodes untrusted media is running native code on attacker input. Image resize endpoints, OG tag generators, thumbnail services. The format does not matter as much as the question of where decoding happens. I still like AVIF. I just no longer want my app server deciding to parse one at request time.

Anyway. Patch, rebuild the image, verify inside the container. The whole drill took me about ten minutes per app, and both are back on 16.3.3 as of writing. Cloudflare rolled emergency WAF rules too, but a WAF rule buys you days, not a pass on upgrading.

If you are also rethinking how much image work your app server should be doing at runtime, I wrote earlier about how server components changed my dashboard's performance profile, which is the same app that got patched this week. Small server, fewer places to fail.