/ Security  ·  August 31, 2026  ·  14 min read

npm audit says you have zero vulnerabilities, and the critical RCE in your image pipeline is still there

On August 25, 2026 Next.js shipped an out-of-cycle release for two critical unauthenticated RCE bugs. One is a Windows-only path traversal, CVE-2026-75604, CVSS 9.0. The other is not a Next.js bug at all: a heap buffer overflow in libheif, CVSS 9.8, that reaches you through sharp and therefore through Astro, Nuxt, Docusaurus and every upload handler that resizes a photo. We checked what the standard tooling reports. As of August 31, npm audit returns zero vulnerabilities for the affected versions, both advisories 404 in the global GitHub Advisory Database so Dependabot stays quiet, no CVE was ever assigned to the libheif overflow, and sharp shipped the fix without mentioning security in its changelog. Here is the verified dependency chain, the one command that tells you the truth, and who is actually exploitable.

By Rushil Shah
SecurityWeb DevelopmentSmall Business

Most security advice for a small business with a website ends at the same instruction: keep your dependencies patched, and let the tooling tell you when something is wrong. Turn on Dependabot. Run npm audit in CI. Fix what it flags.

That is good advice and it is what we tell clients. This week it produced a wrong answer, and the way it produced a wrong answer is worth more of your attention than the vulnerability itself.

On August 25, 2026, the Next.js team published an out-of-cycle security release covering two critical severity vulnerabilities, both leading to unauthenticated remote code execution. We went and checked what the ordinary tooling says about the affected versions. Six days later, on August 31, here is what it says:

Check What it reports
npm audit on next@16.3.2 (vulnerable) 0 vulnerabilities
npm audit on next@15.5.23 (vulnerable) 0 vulnerabilities
npm audit on sharp@0.35.3 (ships vulnerable libheif) 0 vulnerabilities
npm advisory API for all three empty object, {}
github.com/advisories/GHSA-p293-qw3h-jr36 HTTP 404
github.com/advisories/GHSA-2xp9-vwfh-vxw4 HTTP 404
CVE assigned to the libheif overflow none
sharp changelog entry mentioning the fix none

Every one of those is a real result from a real command, run while writing this. The vulnerabilities are real too. Both things are true at once, and that gap is the actual story.

The two Next.js bugs, briefly

CVE-2026-75604, Windows path traversal, CVSS 9.0. Applications using both the Pages Router and the App Router without Cache Components, running on a Windows filesystem, can be pushed into unauthenticated remote code execution. Affected: Next.js 13.4 through 15.5.23, and 16.0 through 16.3.2. Patched in 15.5.24 and 16.3.3. Vercel’s advisory is blunt about the options: “There is no known workaround for affected windows-hosted applications.” Linux and macOS are not affected.

If you host Next.js on IIS, on a Windows VM, or on a Windows container because that is what your line-of-business stack runs on, this is your Monday morning. If you are on Linux, which is nearly everyone, skip it.

The AVIF one, no CVE, CVSS 9.5 in Next.js terms and 9.8 upstream. This is the one that matters more, because it is not a Next.js bug. Next.js uses the sharp image processing package. sharp uses libvips. libvips uses libheif to decode HEIF, HEIC and AVIF files. The bug is in libheif, and Next.js is simply one of thousands of things that call it.

The patched Next.js releases do not fix it. They disable AVIF optimisation entirely as a stopgap, in the maintainers’ words, “until an upstream fix is propagated.”

What the libheif bug actually is

Tracked as GHSA-g89c-p67h-r497, affecting libheif up to and including v1.23.1, fixed in v1.23.2. CVSS 9.8. No CVE assigned.

It is a heap buffer overflow, an out-of-bounds write, in HeifPixelImage::scale_nearest_neighbor(). The mechanism is a four-step failure that is worth reading if you ever write code that parses attacker-supplied file formats, because none of the four steps is obviously a bug on its own:

  1. transfer_channel_from_image_as() appends source planes without rejecting a channel that already exists on the destination. The upstream source carries an unresolved // TODO at exactly that spot.
  2. find_storage_for_channel() returns only the first match, so the functions that report bit depth and memory size describe only the first alpha plane even when duplicates exist at different bit depths.
  3. The scaler therefore allocates a destination alpha plane sized for the first entry, typically 8 bits per sample, then iterates every source entry. Reaching a 10 or 12-bit duplicate, it casts the 8-bit buffer to uint16_t* and writes two bytes per sample into a one-byte-per-sample allocation.
  4. Nested identity-derivation (iden) and auxiliary (auxl) item references are what create those duplicates, and check_decoded_image_size() returns success unconditionally, so the dimension mismatch that forces the scaling path is never rejected.

A crafted file with five items produces an allocation of 16,384 bytes and a write of 32,768 bytes. The overflow length and the bytes written are both attacker-controlled through the container structure and the HEVC bitstream.

The upstream advisory is explicit about the blast radius: any application calling heif_decode_image() is affected, with no special API options or unusual calling patterns required. That last clause is the one to hold on to.

A correction to the coverage

Some reporting on this said the AVIF issue is “only enabled when sites explicitly configure image/avif in next.config.js.” That is worth pulling apart, because it is the difference between “not my problem” and “my problem.”

The formats setting in Next.js is real and the default really is formats: ['image/webp'], with AVIF opt-in. But formats controls the output format that the optimiser produces. The libheif bug fires on decode. If an AVIF or HEIC file reaches the optimiser as input, libheif parses it regardless of what you asked the output to be. That is what “any application calling heif_decode_image()” means.

So the safer question is not “did I turn on AVIF output” but “can anyone hand my server an image file whose bytes they chose.” We flag this because the two questions have very different answers for a site with user uploads.

A second correction: at least one outlet reported on August 27 that libheif v1.23.2 had not been published. It had. The release atom feed timestamps it 2026-08-25T11:39:19Z, the same day as the Next.js release.

The dependency chain, verified

This is the part we could not find written down anywhere, so we tested it. Two throwaway installs, one command each:

npm i sharp@0.35.3 && node -e "console.log(require('sharp').versions.heif)"
# 1.23.1   <- vulnerable

npm i sharp@0.35.4 && node -e "console.log(require('sharp').versions.heif)"
# 1.23.2   <- patched

So the fix does exist for everyone downstream of sharp, and it landed fast:

Date Release libheif inside
Aug 25, 2026 libheif v1.23.2 1.23.2
Aug 25, 2026 Next.js 16.3.3 / 15.5.24 AVIF path disabled
Aug 26, 2026 sharp-libvips v1.3.3 1.23.2
Aug 26, 2026 sharp v0.35.4 1.23.2

sharp 0.35.4 is the fix. Now read its changelog entry for that release. It lists six items: bounding resize dimensions to a coordinate limit, bounding composite left and top, rounding palette bit depth for png and gif colours, a tiff subifd input option, correcting info.pages, and stream handling. It links the sharp-libvips v1.3.3 release. It does not contain the words security, libheif, AVIF, or overflow.

Nothing improper happened there. sharp’s changelog documents sharp’s changes, and this was a prebuilt-binary dependency bump. But if you are the person who reads release notes before upgrading, and you read that entry, you would reasonably conclude 0.35.4 is a routine patch you can leave until next sprint.

Why nothing told you

Four independent gaps line up, and any one of them alone would have been survivable.

No CVE. The security bulletin covering the libheif 1.23.2 release lists eight vulnerabilities in versions 1.0.0 through 1.23.1, including the CVSS 9.3 heap overflow and two more out-of-bounds writes rated 8.4 and 8.3. Every one is listed with no CVE ID assigned. If your vulnerability management process keys on CVE numbers, and most do, there is nothing to match against.

Not in the global GitHub Advisory Database. Both Next.js advisories exist, but as repository advisories on vercel/next.js. Fetch either GHSA from github.com/advisories/ and you get a 404, verified on August 31. That matters mechanically, not just cosmetically: GitHub’s own documentation states that only advisories reviewed by GitHub trigger Dependabot alerts, and that Dependabot does not create alerts for unreviewed advisories. A repository advisory that has not been promoted to the global database is not going to page you.

Not in the npm advisory database. The endpoint npm audit calls returns an empty object for next@16.3.2, next@15.5.23 and sharp@0.35.3. We control-tested the same endpoint against lodash@4.17.15 and it returned the expected advisories immediately, so the query works and the silence is real. There is even precedent for what a fix would look like: npm already carries advisories titled “sharp vulnerability in libwebp dependency CVE-2023-4863” and “sharp inherited vulnerabilities in libvips.” The mechanism exists for exactly this situation. It just has not been used here yet.

Silent in the changelog, as above.

The result is a critical unauthenticated RCE in a dependency that around 45 million weekly downloads pass through, where every automated check a normal team runs reports green.

We have made this argument before in a different key, when we wrote about CISA moving to risk-based patching and about npm supply chain attacks. The through-line is the same: automated dependency scanning is a floor, not a ceiling. It catches the things that made it into a database. It is structurally blind to the window between disclosure and cataloguing, and that window is exactly when exploitation is cheapest.

Are you actually exploitable

Being on a vulnerable version and being exploitable are different states. The gate is simple: can an unauthenticated stranger choose the bytes of an image your server decodes?

Ordered roughly by how worried to be:

High. You accept image uploads. Profile photos, product images from vendors, attachments on a contact form, anything that hits a resize pipeline. This is the case the bug was written for. The attacker picks the file.

High. Your Next.js images.remotePatterns or images.domains includes a wildcard, a user-supplied hostname, or a third party you do not control. The optimiser will fetch and decode whatever is at that URL.

Medium. You proxy or hotlink images from a CMS, a marketplace feed, a syndication partner, or any source where someone other than you can replace a file. A WordPress media library with contributor accounts counts.

Low. Every image is a local file committed to your repository, remotePatterns is empty, and there is no upload path anywhere. The vulnerable code is present and will never be handed hostile input by an outsider. Patch it on your normal cadence, but do not lose an evening.

Separate and unrelated to all of the above: if you are hosting Next.js on Windows, CVE-2026-75604 does not care about any of this, and there is no workaround. Upgrade.

Where you host changes the picture too, and both major platforms published clear statements. Vercel disabled AVIF optimisation across its managed image service when the bug was identified and its Next.js runtime is Linux, so it says applications hosted there need no upgrades, configuration changes or redeploys. Netlify rewrites /_next/image requests to its own Image CDN at the edge, so the Next.js optimisation API is never invoked, and its functions run on Linux. Netlify still recommends upgrading as good practice. If you self-host on your own VM or container, neither of those mitigations applies to you and you own the whole problem.

What to do this week

1. Find out what libheif you are actually running. Not what your lockfile says about sharp, what the binary reports:

node -e "const s=require('sharp'); console.log('sharp', s.versions.sharp, '| libvips', s.versions.vips, '| libheif', s.versions.heif)"

Anything below 1.23.2 is the vulnerable library. Run this in every project that processes images, not just the ones you think of as “the website.”

2. Upgrade sharp, and upgrade Next.js if you use it.

npm install sharp@^0.35.4
npm install next@16.3.3   # or next@15.5.24 on the maintenance line

Then re-run the version check above. On pnpm or yarn workspaces, confirm the transitive @img/sharp-libvips-* packages actually moved to 1.3.3, because a stale lockfile will happily keep the old prebuilt binary. The version check is the proof; the lockfile is the claim.

3. Remember that sharp is not only a Next.js dependency. It is the default image processor for Astro, it sits under Nuxt Image, Docusaurus, Gatsby, Remix setups, next-optimized-images, and a very large number of hand-rolled upload handlers that call sharp(buffer).resize().toBuffer(). Neither Astro nor most of those projects has published an advisory for this, which is not negligence; there is nothing for them to advise about beyond “upgrade sharp,” which you can do yourself today. The comparison we drew in Astro vs Next.js vs WordPress holds here in an uncomfortable way: the frameworks differ enormously at the top of the stack and converge on exactly the same C library at the bottom.

4. If you cannot upgrade immediately, reject HEIF-family input before it reaches sharp. Check the container brand rather than trusting the file extension or the client’s Content-Type. In an ISOBMFF file, bytes 4 through 8 are the literal string ftyp and bytes 8 through 12 are the brand:

function isHeifFamily(buf) {
  if (buf.length < 12) return false;
  if (buf.toString('latin1', 4, 8) !== 'ftyp') return false;
  const brand = buf.toString('latin1', 8, 12);
  return ['avif', 'avis', 'heic', 'heix', 'hevc', 'hevx', 'mif1', 'msf1'].includes(brand);
}

This is a stopgap, not a fix. It reduces the surface while you schedule the upgrade properly. Do the upgrade.

5. Check your distro packages separately. libheif is not only an npm concern. It is linked into ImageMagick’s HEIF delegate, GNOME thumbnailers, Nextcloud previews and a long tail of desktop and server software. Ubuntu’s USN-8649-1 on August 19 fixed two denial-of-service issues, CVE-2026-62289 and CVE-2026-62292, shipping 1.21.2-3ubuntu0.4 on 26.04 LTS and 1.17.6-1ubuntu4.7 on 24.04 LTS. Both of those base versions predate 1.23.2, and the critical overflow carries no CVE for a distro tracker to key on. Check your own vendor’s tracker rather than assuming a recent update covered it.

6. Write down who is responsible for image processing. This is the boring one and it is the one that pays. Every business we have helped clean up after a dependency incident had the same gap: the image pipeline was set up once, by someone, during a project, and nobody owned it afterwards. It is not in the asset inventory. It has no release notifications going anywhere. The fix is an afternoon, once.

What we did on our own site

The site you are reading is Astro, and it uses sharp. We are not going to write this post and skip our own check, so here is what it returned before we published.

Our lockfile had sharp@0.35.3, with @img/sharp-libvips-*@1.3.2 underneath it. Running the version command against the installed binary:

BEFORE -> sharp 0.35.3 | libvips 8.18.3 | libheif 1.23.1

Vulnerable library, present in our build. npm audit on that same tree returned zero vulnerabilities.

Exploitable? No, and the reason is worth being precise about. Our astro.config.mjs sets image.domains: [] and image.remotePatterns: [], every image on this site is a local file committed to the repository, and there is no upload path anywhere on it. An outsider has no way to hand our build a file they wrote. That is the “Low” row in the table above, and it is low because of a configuration decision made months ago for unrelated reasons, not because anything warned us.

We upgraded anyway, in the same commit as this post:

AFTER -> sharp 0.35.4 | libvips 8.18.6 | libheif 1.23.2

Which is the point of the whole exercise. We were not exploitable, and we still had a critical heap overflow sitting in a linked C library in our own dependency tree, six days after the fix shipped, with every automated check green. We only knew because we went and read a changelog that does not mention it, in a project three levels below the one that made the news.

The uncomfortable general lesson

The industry has spent five years telling small businesses that dependency scanning is the answer to supply chain risk, and it is a genuinely good answer for the steady state. But it is a lagging indicator by construction. A scanner can only report what a human has already written into a database, in a format the scanner can match, against a package name the scanner knows about.

This bug failed all three conditions simultaneously. It had no CVE for a matcher to match. It sat in a repository advisory the global database had not ingested. And it lived in a C library that is not an npm package at all, reached through two layers of prebuilt binaries that no manifest names.

None of that is anyone’s fault in particular. Advisories take time to review. Maintainers ship the fix before the paperwork. But it means the honest version of the advice is longer than “turn on Dependabot”:

  • Know what your software actually links against, not just what your package.json declares. sharp.versions took one command and told us more than any scanner did.
  • Treat a dependency bump in a security-adjacent library as security-relevant even when the changelog is silent about it.
  • When a framework you use ships an out-of-cycle release, read the upstream advisory it points at, not the summary. The upstream advisory here was the one that said “any application calling heif_decode_image().”
  • Accept that for roughly a week after any disclosure, your tooling is going to tell you that you are fine.

If you are not sure what your site’s image pipeline links against, or who owns it, or whether anyone would notice a release like this one, get in touch. Running the check is a fifteen-minute job. It is a much shorter job than the alternative.

Sources

● Taking new projects

Have something that needs shipping?

One call. Thirty minutes. You leave with an honest read on scope, timeline, and price, whether we're the right fit or not.