6 min readJohnny UnarJohnny Unar

Your Image Optimization Pipeline Is a Remote Code Execution Surface

The Next.js AVIF CVE wasn't in Next.js. It lived four dependencies deep, in code nobody on your team has ever read, and it ran on every image request.

the CVE nobody wrote

On August 25, 2026, Vercel disclosed a CVSS 9.5 flaw affecting Next.js image optimization, and if you read only the advisory headline you'd assume someone at Vercel shipped a bug in next/image. They didn't. The vulnerability lived in libheif, the AVIF and HEIC decoding library, which gets pulled in transitively through sharp, which next/image invokes silently on every single optimization request unless you've gone out of your way to turn it off. So the chain looks like this: an attacker sends a crafted AVIF file to your /_next/image endpoint, Next.js hands it to sharp, sharp hands it to libheif, and libheif has a heap overflow in its box parsing that lets the attacker corrupt memory in the same process that's serving your app. You patch to 15.5.24 or 16.3.3 and the advisory closes, but the interesting part isn't the patch, it's what the patch reveals about how many layers of code you're executing on attacker-controlled input without ever having looked at any of it. The people who found this bug were almost certainly running LLM-assisted fuzzing against the libheif box parser, because that's the kind of deeply nested, format-specific overflow that used to take a human researcher weeks to isolate and now falls out of an afternoon of automated harness generation. That timeline matters, and I'll come back to it.

why 'we use a well-known library' is not a defense

Every time one of these lands, someone in the incident channel says some version of 'but sharp is battle-tested, it's the standard, everyone uses it.' That defense is worthless, and it's worth being precise about why. sharp is genuinely well-maintained. libvips underneath it is well-maintained. The problem is that neither of them controls libheif, and libheif is a C++ library parsing an ISO base media file format that was designed for video containers and then got bolted onto still images, which means it carries an enormous surface of box types, extents, and reference structures that an attacker can nest and overlap in ways the original authors never fully tested. When you say 'we trust sharp,' what you're actually saying is 'we trust sharp, and libvips, and libheif, and libde265, and whatever those depend on, transitively, forever, including future versions we'll auto-update into.' The trust doesn't stop at the boundary you can see. Any public endpoint that decodes attacker-supplied media is an RCE candidate by default, and the number of well-known libraries in the chain is not a mitigating factor, it's a measure of how many independent teams have to never make a memory safety mistake for you to stay safe. That's not a bet I want to make on a Django or Next.js app that's exposed to the open internet, and we've stopped making it for the clients we run infrastructure for.

auditing your own pipeline

Start by figuring out whether your Next.js app is even calling sharp, because if you're on Vercel's managed image optimization the decoding happens on their infrastructure and the blast radius is theirs, not yours. If you're self-hosting, and a lot of you reading this are running Next.js 15.x or 16.x on your own ECS tasks or bare Hetzner boxes, then next/image is running sharp in your process and you own the exposure. Check your next.config.js for the images block. If you haven't explicitly restricted formats, next/image will happily accept and decode AVIF, which is the exact path this CVE exercises. You can set formats: ['image/webp'] to drop AVIF output, but be careful, because that controls what Next.js produces, not necessarily every decode path for what it ingests, so the safer move is to constrain what you accept at the edge. If your users upload images, and those images hit sharp anywhere in your stack, treat that pipeline as untrusted execution and put it behind a boundary. We've moved this kind of decoding into a separate worker for a couple of clients running document processing pipelines, a small Go service that shells out to a hardened sharp process with a memory limit and no network egress, so that a successful overflow gets you a crashed sandbox instead of a shell on the box that holds the Postgres credentials. Also actually run npm ls sharp and npm ls libheif or the equivalent, because half the teams I've talked to about this didn't realize sharp was even in their tree until they looked.

what a monthly security cadence signals

Vercel quietly moved to a monthly security release cadence this year, and I don't think enough people clocked what that actually means. You don't schedule regular security releases because you suddenly got sloppy. You schedule them because the rate of inbound disclosures crossed a threshold where ad-hoc patching stopped scaling, and that threshold got crossed because vulnerability research is being automated. The libheif overflow is a textbook example of the kind of bug that LLM-assisted fuzzing surfaces at volume: format parsers with deep state, lots of edge cases, and decades of accreted code that no human has audited end to end. Researchers are now generating fuzz harnesses, triaging crashes, and even drafting the reproduction cases with model assistance, which compresses the discovery timeline from weeks to hours and multiplies the number of people who can do it. The consequence for you is that the gap between 'this bug exists in your dependency tree' and 'this bug is public and being scanned for' is shrinking fast, and a quarterly dependency-bump ritual is no longer fast enough. If you're auto-scanning Shodan for exposed Next.js image endpoints, and people are, the window between a CVSS 9.5 disclosure and mass exploitation attempts is measured in days now. Plan your patch cadence around that, not around your old assumptions about how long you have.

what to actually do this week

Patch to 15.5.24 or 16.3.3, obviously, and do it today if your app is public and decodes uploaded or remote images. That closes the immediate hole. Then do the harder work, which is deciding whether you want to keep running media decoders in the same process and privilege context as your application logic at all. My honest position is that you shouldn't, not for anything internet-facing, because the next libheif-class bug is already sitting in some codec in your tree waiting for an automated fuzzer to find it, and the patch you apply next month won't help you against the one disclosed the month after. Move decoding behind a process boundary with a memory cap and no secrets and no network. Restrict the formats you accept to the ones you genuinely need. Turn off AVIF ingestion if nothing in your product depends on it, because a format you don't accept is a parser you never invoke. And build a real deploy path for security patches so that shipping a point release is a fifteen minute operation, not a two day change-management saga, because the cadence of these disclosures is only going one direction. If you want a second set of eyes on your image pipeline or your dependency exposure generally, that's the kind of security audit we do at steezr, and it usually pays for itself the first time it keeps a crafted AVIF from turning into a shell on your infrastructure.

Johnny Unar

Written by

Johnny Unar

Want to work with us?

The Next.js AVIF CVE wasn't in Next.js. It lived four dependencies deep, in code nobody on your team has ever read, and it ran on every image request.