Guide

Next.js security update, October 2026: check and patch CVE-2026-96889

By Aman Singh · Published · 10 min read

On 14 October 2026 Vercel released Next.js 16.4.1 and 15.5.28 to fix two critical remote code execution flaws and a denial-of-service bug. One of them, CVE-2026-96889, sits in the image optimizer that most Next.js sites use through next/image. This guide shows which apps are affected, gives a small script that checks a project's installed versions and features, and covers the one upgrade path that leaves the image flaw in place.

This site runs on Next.js, and was on 16.3.0 until today. The upgrade I ran is in the guide. The checker was tested on 23 October 2026 with Node.js 26 against real installs of Next.js 16.3.0, 15.5.28 and 16.4.1, and against fake npm and pnpm projects.

The Node.js runtime under Next.js needs this month's fix too: Node2Shell (Brotli) check and patch →

What was fixed

AdvisoryWhatSeverityAffected Next.js
CVE-2026-96889Remote code execution in the Image Optimization API with SVG files, through sharp, on glibc-based LinuxCritical, 9.510.0.0 to 15.5.27, 16.0.0 to 16.4.0
GHSA-673q-mjm6-r5xrUnauthenticated remote code execution when a "use cache" function returns multiple typed arrays (App Router with Cache Components)Critical, 9.516.0.0 to 16.4.0
GHSA-p3m5-59mp-jv5rCrafted requests to Server Actions cause excessive CPU use, blocking the processHigh, 8.213.0.0 to 15.5.27, 16.0.0 to 16.4.0

Fixed releases: 16.4.1 and 15.5.28. The image flaw's fix is sharp 0.35.5, and the two Next.js 16 flaws come from React Server Components code that React fixed in 19.0.9, 19.1.10, 19.2.9 and 19.3.1.

Is your app affected?

Your appAffected byAction
Next.js 16, anything below 16.4.1All three (use cache and Server Actions only if you use them)Upgrade to 16.4.1
Next.js 15 below 15.5.28, with Server ActionsImage RCE and Server Actions DoSUpgrade to 15.5.28, check sharp
Next.js 15.5.28 with sharp 0.34.xImage RCEInstall sharp 0.35.5 or later
Pages Router only, no Server ActionsImage RCE onlyUpgrade, or update sharp
Next.js 14 or olderImage RCE (and Server Actions DoS from 13.0)No fixed release: move to 15.5.28 or 16.4.1

Vercel's advisory says to upgrade even if your app has no use cache functions, so treat every Next.js 16 app below 16.4.1 as needing the update.

Check your project with a script

package.json shows ranges, not what is installed, and sharp can be installed more than once. This script reads every installed copy of next, sharp and react-server-dom-* (npm, pnpm or yarn layouts), looks through your source for "use cache", "use server" and next/image, and reports each advisory. It has no dependencies.

The script and its tests are also on GitHub as AmanSin001/next-security-check (MIT licence). Download it with curl -O https://raw.githubusercontent.com/AmanSin001/next-security-check/main/next-security-check.mjs, or copy it from below.

next-security-check.mjs
#!/usr/bin/env node
// next-security-check.mjs: checks a Next.js project against the October 14,
// 2026 advisories (CVE-2026-96889, GHSA-673q-mjm6-r5xr, GHSA-p3m5-59mp-jv5r).
// No dependencies. Run it in the project root, after installing packages:
//   node next-security-check.mjs [path]
// It reads the versions actually installed in node_modules (npm, pnpm or yarn),
// not package.json ranges. Exit code: 0 = patched, 1 = action needed.
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

// --- version helpers ---------------------------------------------------------
export function parse(version) {
  const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version ?? "");
  return m ? { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] ?? "" } : null;
}

export function compare(a, b) {
  const x = parse(a);
  const y = parse(b);
  for (const k of ["major", "minor", "patch"]) if (x[k] !== y[k]) return x[k] - y[k];
  if (x.pre === y.pre) return 0;
  if (!x.pre) return 1; // a release is newer than its pre-releases
  if (!y.pre) return -1;
  return x.pre < y.pre ? -1 : 1;
}

const between = (v, lo, hi) => compare(v, lo) >= 0 && compare(v, hi) <= 0;

// --- the advisories ----------------------------------------------------------
// Returns one row per advisory: { id, title, affected, fixedBy, applies, note }
export function assess({ next, sharp = [], rsd = [], features }) {
  const rows = [];
  const nextFix = next && parse(next).major >= 16 ? "16.4.1" : "15.5.28";

  // CVE-2026-96889: RCE in Image Optimization with SVG, via sharp (glibc Linux).
  const imgRange = next && (between(next, "10.0.0", "15.5.27") || between(next, "16.0.0", "16.4.0"));
  const oldSharp = sharp.filter((v) => compare(v, "0.35.5") < 0);
  rows.push({
    id: "CVE-2026-96889",
    title: "Image Optimization RCE with SVG (critical)",
    // sharp is the real fix: 15.5.28 still accepts sharp ^0.34.3, and 0.34.x has
    // no fixed release, so an old sharp is a problem on any Next.js version.
    inRange: Boolean(imgRange) || oldSharp.length > 0,
    affected: Boolean(next && compare(next, "10.0.0") >= 0) && oldSharp.length > 0,
    fixedBy: imgRange ? `next ${nextFix}, or sharp 0.35.5+ for every copy` : "sharp 0.35.5+ for every copy (npm install sharp@^0.35.5)",
    applies: features.nextImage || features.svgAllowed ? "your app uses next/image" : "no next/image found, but the endpoint still exists",
    note:
      imgRange && oldSharp.length === 0
        ? `next ${next} is in range, but every sharp copy is 0.35.5+ (the documented workaround).`
        : oldSharp.length
          ? `sharp ${oldSharp.join(", ")} installed; needs 0.35.5+${imgRange ? "" : " (this Next.js still accepts it: run npm install sharp@^0.35.5)"}.`
          : "",
  });

  // GHSA-673q-mjm6-r5xr: RCE in `use cache` returning typed arrays (16.x only).
  const cacheRange = next && between(next, "16.0.0", "16.4.0");
  rows.push({
    id: "GHSA-673q-mjm6-r5xr",
    title: "use cache RCE (critical, Next.js 16)",
    inRange: Boolean(cacheRange),
    affected: Boolean(cacheRange),
    fixedBy: "next 16.4.1",
    applies: features.useCache ? "your code has \"use cache\"" : "no \"use cache\" found; Vercel still says upgrade",
    note: "",
  });

  // GHSA-p3m5-59mp-jv5r: Server Actions denial of service.
  const dosRange = next && (between(next, "13.0.0", "15.5.27") || between(next, "16.0.0", "16.4.0"));
  rows.push({
    id: "GHSA-p3m5-59mp-jv5r",
    title: "Server Actions denial of service (high)",
    inRange: Boolean(dosRange),
    affected: Boolean(dosRange) && features.serverActions,
    fixedBy: `next ${nextFix}`,
    applies: features.serverActions ? "your code has \"use server\"" : "no Server Actions found: not vulnerable",
    note: dosRange && !features.serverActions ? "In the affected range, but only apps with Server Actions are vulnerable." : "",
  });

  // React Server Components packages installed directly (GHSA-xqc2-xf4c-x3h3).
  const rsdFixed = { "19.0": "19.0.9", "19.1": "19.1.10", "19.2": "19.2.9", "19.3": "19.3.1" };
  const badRsd = rsd.filter(({ version }) => {
    const p = parse(version);
    const fix = p && rsdFixed[`${p.major}.${p.minor}`];
    return fix && compare(version, fix) < 0;
  });
  rows.push({
    id: "GHSA-xqc2-xf4c-x3h3",
    title: "React Server Components packages (critical)",
    inRange: badRsd.length > 0,
    affected: badRsd.length > 0,
    fixedBy: "react-server-dom-* 19.0.9 / 19.1.10 / 19.2.9 / 19.3.1",
    applies: rsd.length ? "installed directly" : "not installed directly (Next.js bundles its own)",
    note: badRsd.map((r) => `${r.name}@${r.version}`).join(", "),
  });

  if (next && parse(next).major < 15 && imgRange) {
    rows[0].note += " Next.js 14 and older get no fixed release: upgrade to 15.5.28 or 16.4.1.";
  }
  return rows;
}

// --- reading the project ---------------------------------------------------------
function readJson(file) {
  try {
    return JSON.parse(readFileSync(file, "utf8"));
  } catch {
    return null;
  }
}

/** Every installed copy of the named packages, including nested and pnpm copies. */
export function findInstalled(root, names) {
  const wanted = (name) => names.some((n) => (n.endsWith("*") ? name.startsWith(n.slice(0, -1)) : name === n));
  const found = [];
  const visited = new Set();

  const walkModules = (dir) => {
    let real;
    try {
      real = realpathSync(dir);
    } catch {
      return;
    }
    if (visited.has(real)) return; // pnpm symlinks can loop
    visited.add(real);
    for (const entry of safeReaddir(dir)) {
      if (entry === ".pnpm") {
        for (const store of safeReaddir(path.join(dir, entry))) walkModules(path.join(dir, entry, store, "node_modules"));
      } else if (entry.startsWith("@")) {
        for (const sub of safeReaddir(path.join(dir, entry))) visitPackage(path.join(dir, entry, sub), `${entry}/${sub}`);
      } else if (!entry.startsWith(".")) {
        visitPackage(path.join(dir, entry), entry);
      }
    }
  };

  const visitPackage = (dir, name) => {
    if (wanted(name)) {
      const pkg = readJson(path.join(dir, "package.json"));
      if (pkg?.version) found.push({ name: pkg.name ?? name, version: pkg.version, path: dir });
    }
    const nested = path.join(dir, "node_modules");
    if (existsSync(nested)) walkModules(nested);
  };

  walkModules(path.join(root, "node_modules"));
  return found;
}

function safeReaddir(dir) {
  try {
    return readdirSync(dir);
  } catch {
    return [];
  }
}

const SOURCE_DIRS = ["app", "src", "pages", "components", "lib"];
const SOURCE_EXT = /\.(m?[jt]sx?)$/;

export function detectFeatures(root) {
  const features = { appRouter: false, useCache: false, serverActions: false, nextImage: false, svgAllowed: false };
  features.appRouter = existsSync(path.join(root, "app")) || existsSync(path.join(root, "src", "app"));
  const scan = (dir) => {
    if (!existsSync(dir)) return;
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
      if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) scan(full);
      else if (SOURCE_EXT.test(entry.name)) {
        const text = readFileSync(full, "utf8");
        if (/["']use cache(?::[^"']*)?["']/.test(text)) features.useCache = true;
        if (/["']use server["']/.test(text)) features.serverActions = true;
        if (/from\s+["']next\/(legacy\/)?image["']/.test(text)) features.nextImage = true;
      }
    }
  };
  for (const d of SOURCE_DIRS) scan(path.join(root, d));
  for (const name of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
    const file = path.join(root, name);
    if (existsSync(file) && /dangerouslyAllowSVG\s*:\s*true/.test(readFileSync(file, "utf8"))) features.svgAllowed = true;
  }
  return features;
}

export function check(root) {
  const installed = findInstalled(root, ["next", "sharp", "react-server-dom-*"]);
  const nextPkg = readJson(path.join(root, "node_modules", "next", "package.json"));
  const next = nextPkg?.version ?? installed.find((p) => p.name === "next")?.version ?? null;
  const sharp = [...new Set(installed.filter((p) => p.name === "sharp").map((p) => p.version))];
  const rsd = installed.filter((p) => p.name.startsWith("react-server-dom-") && !p.path.includes(`${path.sep}next${path.sep}`));
  const features = detectFeatures(root);
  return { next, sharp, rsd, features, rows: assess({ next, sharp, rsd, features }) };
}

// --- CLI ---------------------------------------------------------------------
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
  const root = path.resolve(process.argv[2] ?? ".");
  const r = check(root);
  if (!r.next) {
    console.log(`No installed next package found under ${root}. Run npm install first.`);
    process.exit(1);
  }
  const f = r.features;
  console.log("Next.js security check (October 14, 2026 advisories)");
  console.log(`next:     ${r.next}`);
  console.log(`sharp:    ${r.sharp.length ? r.sharp.join(", ") : "not installed"}`);
  console.log(`features: App Router ${f.appRouter ? "yes" : "no"}, "use cache" ${f.useCache ? "yes" : "no"}, Server Actions ${f.serverActions ? "yes" : "no"}, next/image ${f.nextImage ? "yes" : "no"}`);
  console.log("");
  let action = false;
  for (const row of r.rows) {
    action ||= row.affected;
    console.log(`${row.affected ? "FIX " : "ok  "} ${row.id.padEnd(20)} ${row.title}`);
    const detail = row.inRange || row.id === "GHSA-xqc2-xf4c-x3h3" ? `${row.applies}${row.note ? `. ${row.note}` : ""}` : `next ${r.next} is not affected`;
    console.log(`      ${detail}`);
    if (row.affected) console.log(`      Fix: ${row.fixedBy}`);
  }
  console.log("");
  console.log(action ? "Action needed: upgrade, reinstall, rebuild and redeploy." : "No action needed for these advisories.");
  process.exit(action ? 1 : 0);
}
Terminal
# in the project root, after npm install / pnpm install / yarn
node next-security-check.mjs
echo "exit code: $?"   # 0 = nothing to do, 1 = action needed

Output on a real Next.js 16.3.0 install with Server Actions and next/image:

Output (Next.js 16.3.0)
Next.js security check (October 14, 2026 advisories)
next:     16.3.0
sharp:    0.35.3
features: App Router yes, "use cache" no, Server Actions yes, next/image yes

FIX  CVE-2026-96889       Image Optimization RCE with SVG (critical)
      your app uses next/image. sharp 0.35.3 installed; needs 0.35.5+.
      Fix: next 16.4.1, or sharp 0.35.5+ for every copy
FIX  GHSA-673q-mjm6-r5xr  use cache RCE (critical, Next.js 16)
      no "use cache" found; Vercel still says upgrade
      Fix: next 16.4.1
FIX  GHSA-p3m5-59mp-jv5r  Server Actions denial of service (high)
      your code has "use server"
      Fix: next 16.4.1
ok   GHSA-xqc2-xf4c-x3h3  React Server Components packages (critical)
      not installed directly (Next.js bundles its own)

Action needed: upgrade, reinstall, rebuild and redeploy.
Show the tests (8 cases, including pnpm and the sharp trap)
next-security-check.test.mjs
// node --test next-security-check.test.mjs
// Builds small fake projects on disk (npm and pnpm layouts) and checks them.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { assess, check, compare } from "./next-security-check.mjs";

function project(files) {
  const root = mkdtempSync(path.join(tmpdir(), "nextcheck-"));
  for (const [file, content] of Object.entries(files)) {
    const full = path.join(root, file);
    mkdirSync(path.dirname(full), { recursive: true });
    writeFileSync(full, typeof content === "string" ? content : JSON.stringify(content));
  }
  return root;
}
const pkg = (name, version) => ({ name, version });
const status = (r) => Object.fromEntries(r.rows.map((row) => [row.id, row.affected]));

test("version comparison", () => {
  assert.ok(compare("16.4.1", "16.4.0") > 0);
  assert.ok(compare("15.5.27", "15.5.28") < 0);
  assert.ok(compare("16.4.0", "16.4.0-canary.3") > 0);
  assert.ok(compare("0.35.10", "0.35.5") > 0);
});

test("unpatched Next.js 16 app with every feature: all three Next.js advisories", () => {
  const root = project({
    "node_modules/next/package.json": pkg("next", "16.3.0"),
    "node_modules/sharp/package.json": pkg("sharp", "0.35.3"),
    "app/page.tsx": 'import Image from "next/image";\nexport default function P() {}',
    "app/actions.ts": '"use server";\nexport async function save() {}',
    "app/data.ts": 'export async function get() { "use cache"; return new Uint8Array(1); }',
  });
  const r = check(root);
  assert.deepEqual(status(r), {
    "CVE-2026-96889": true,
    "GHSA-673q-mjm6-r5xr": true,
    "GHSA-p3m5-59mp-jv5r": true,
    "GHSA-xqc2-xf4c-x3h3": false,
  });
  assert.deepEqual(r.features, { appRouter: true, useCache: true, serverActions: true, nextImage: true, svgAllowed: false });
  rmSync(root, { recursive: true });
});

test("patched Next.js 16.4.1 with sharp 0.35.5: nothing to do", () => {
  const root = project({
    "node_modules/next/package.json": pkg("next", "16.4.1"),
    "node_modules/sharp/package.json": pkg("sharp", "0.35.5"),
    "app/actions.ts": '"use server";',
  });
  assert.ok(check(root).rows.every((row) => !row.affected));
  rmSync(root, { recursive: true });
});

test("the 15.x lockfile trap: next 15.5.28 but an old nested sharp", () => {
  const root = project({
    "node_modules/next/package.json": pkg("next", "15.5.28"),
    "node_modules/next/node_modules/sharp/package.json": pkg("sharp", "0.34.4"),
  });
  const r = check(root);
  assert.deepEqual(r.sharp, ["0.34.4"]);
  // next 15.5.28 accepts sharp ^0.34.3 and 0.34.x has no fix, so this is flagged.
  assert.equal(status(r)["CVE-2026-96889"], true);
  assert.ok(r.rows[0].note.includes("npm install sharp@^0.35.5"));
  assert.equal(status(r)["GHSA-p3m5-59mp-jv5r"], false);
  rmSync(root, { recursive: true });
});

test("sharp workaround on an unpatched 15.x: image RCE cleared, DoS remains", () => {
  const root = project({
    "node_modules/next/package.json": pkg("next", "15.4.6"),
    "node_modules/sharp/package.json": pkg("sharp", "0.35.5"),
    "app/form.tsx": "'use server'",
  });
  const s = status(check(root));
  assert.equal(s["CVE-2026-96889"], false);
  assert.equal(s["GHSA-673q-mjm6-r5xr"], false); // 16.x only
  assert.equal(s["GHSA-p3m5-59mp-jv5r"], true);
  rmSync(root, { recursive: true });
});

test("Pages Router app without Server Actions is not hit by the DoS", () => {
  const root = project({
    "node_modules/next/package.json": pkg("next", "14.2.30"),
    "node_modules/sharp/package.json": pkg("sharp", "0.33.5"),
    "pages/index.tsx": "export default function Home() {}",
  });
  const r = check(root);
  assert.equal(r.features.appRouter, false);
  assert.equal(status(r)["GHSA-p3m5-59mp-jv5r"], false);
  assert.equal(status(r)["CVE-2026-96889"], true);
  assert.match(r.rows[0].note, /no fixed release/);
  rmSync(root, { recursive: true });
});

test("pnpm layout: finds packages inside node_modules/.pnpm, survives symlinks", () => {
  const root = project({
    "node_modules/.pnpm/next@16.2.2/node_modules/next/package.json": pkg("next", "16.2.2"),
    "node_modules/.pnpm/sharp@0.35.1/node_modules/sharp/package.json": pkg("sharp", "0.35.1"),
    "node_modules/.pnpm/react-server-dom-webpack@19.2.4/node_modules/react-server-dom-webpack/package.json": pkg("react-server-dom-webpack", "19.2.4"),
  });
  try {
    symlinkSync(path.join(root, "node_modules/.pnpm/next@16.2.2/node_modules/next"), path.join(root, "node_modules/next"), "junction");
    symlinkSync(path.join(root, "node_modules"), path.join(root, "node_modules/.pnpm/next@16.2.2/node_modules/next/node_modules"), "junction");
  } catch {
    // symlinks unavailable: the .pnpm walk alone must still work
  }
  const r = check(root);
  assert.equal(r.next, "16.2.2");
  assert.deepEqual(r.sharp, ["0.35.1"]);
  assert.equal(status(r)["GHSA-xqc2-xf4c-x3h3"], true);
  rmSync(root, { recursive: true });
});

test("assess flags old React Server Components packages per minor line", () => {
  const rows = assess({
    next: "16.4.1",
    sharp: ["0.35.5"],
    rsd: [{ name: "react-server-dom-webpack", version: "19.1.10" }, { name: "react-server-dom-parcel", version: "19.3.0" }],
    features: { appRouter: true, useCache: false, serverActions: false, nextImage: false, svgAllowed: false },
  });
  const rsdRow = rows.find((row) => row.id === "GHSA-xqc2-xf4c-x3h3");
  assert.equal(rsdRow.affected, true);
  assert.equal(rsdRow.note, "react-server-dom-parcel@19.3.0");
});
node --test next-security-check.test.mjs
ok 1 - version comparison
ok 2 - unpatched Next.js 16 app with every feature: all three Next.js advisories
ok 3 - patched Next.js 16.4.1 with sharp 0.35.5: nothing to do
ok 4 - the 15.x lockfile trap: next 15.5.28 but an old nested sharp
ok 5 - sharp workaround on an unpatched 15.x: image RCE cleared, DoS remains
ok 6 - Pages Router app without Server Actions is not hit by the DoS
ok 7 - pnpm layout: finds packages inside node_modules/.pnpm, survives symlinks
ok 8 - assess flags old React Server Components packages per minor line
# pass 8
# fail 0

The source scan is a guide, not proof: code generated at build time or living in another package will not show up. When in doubt, upgrade.

Patch it

  1. 1
    Upgrade. Keep eslint-config-next on the same version as next.
    Terminal
    # Next.js 16
    npm install next@16.4.1 eslint-config-next@16.4.1
    
    # Next.js 15
    npm install next@15.5.28
    npm install sharp@^0.35.5   # only if sharp is in your own package.json
    
    # then confirm what actually got installed
    npm ls next sharp
  2. 2
    Run the checker again and commit the lockfile, so CI and production install exactly what you tested.
  3. 3
    Build and test. Run a full production build and lint, and click through pages that use images, forms and cached data.
  4. 4
    Redeploy and confirm. Vercel and other platforms build from the new lockfile on deploy. Self-hosted Node servers and Docker images must be rebuilt and restarted. In the browser console of the live site, window.next.version shows the version that is actually running.

While you redeploy, check the Node.js runtime too. A week later, on 21 October, Node.js fixed a Brotli decoder flaw that can lead to remote code execution; see the Node.js Brotli vulnerability guide and the October 2026 security roundup for everything that shipped this month.

The sharp trap on Next.js 15

The image fix lives in sharp. Next.js 16.4.1 forces the fixed sharp; 15.5.28 accepts an older one, so check sharp separately.

Next.js 16.4.1 requires sharp ^0.35.5, so upgrading pulls in the fixed version. Next.js 15.5.28 accepts ^0.34.3 || ^0.35.5, and the last 0.34 release (0.34.5) came out in November 2025, before the fix. If sharp is already in your own package.json (older Next.js docs recommended adding it for self-hosting) or pinned in the lockfile, the upgrade leaves it alone. I reproduced this with a real install:

Terminal (real install, 23 October 2026)
$ npm install next@15.5.28
$ node next-security-check.mjs
next:     15.5.28
sharp:    0.34.5
FIX  CVE-2026-96889       Image Optimization RCE with SVG (critical)
      your app uses next/image. sharp 0.34.5 installed; needs 0.35.5+
      (this Next.js still accepts it: run npm install sharp@^0.35.5).

$ npm install sharp@^0.35.5
$ node next-security-check.mjs
...
No action needed for these advisories.

How I patched this site

codewares.in is a Next.js 16 App Router site using next/image, with no Server Actions or use cache. It was on Next.js 16.3.0 and sharp 0.35.3, so the image flaw applied. The upgrade:

Terminal
npm install next@16.4.1 react@19.2.9 react-dom@19.2.9 sharp@^0.35.5
npm install -D eslint-config-next@16.4.1
npm ls sharp
# codewares@0.1.0
# +-- next@16.4.1
# | `-- sharp@0.35.5 deduped
# `-- sharp@0.35.5
  • React too. I moved react and react-dom to 19.2.9 so every React package is on a patched line, although Next.js bundles its own server components runtime.
  • npm changed exact pins to ranges. npm install rewrote "16.3.0" as "^16.4.1"; I restored exact versions so builds stay reproducible.
  • npm audit then showed one production fix (source-map-js, applied with npm audit fix) and five development-only findings in the ESLint toolchain, where the suggested "fix" would downgrade eslint-config-next to 14. I left those.
  • A full lint caught one old plain <a> link to an internal page, now a Link.
  • Result: two clean production builds, every page, social image and the image optimizer checked locally, then deployed. The live site reports window.next.version = 16.4.1.

Next.js 14 and older

There is no fixed 14.x or older release. The image flaw's documented workaround is sharp 0.35.5 or later, but older Next.js versions may not work with it, and the Server Actions issue reaches back to 13.0. The real fix is moving to 15.5.28 or 16.4.1. If that is a large jump, start with the official codemods and fix what the build reports.

Common questions

Which Next.js versions fix CVE-2026-96889?+
Next.js 16.4.1 and 15.5.28, released on 14 October 2026. The fix is in sharp 0.35.5, so also make sure every installed copy of sharp is 0.35.5 or later.
How do I check if my Next.js app is vulnerable?+
Run npm ls next sharp to see what is installed, or the checker script in this guide, which also looks for "use cache", Server Actions and next/image in your code and reports each advisory.
Is my app on Vercel safe without upgrading?+
Vercel's announcement asks everyone to upgrade their Next.js dependency. Upgrading and redeploying is the safe answer wherever the app is hosted.
I don't use "use cache" or Server Actions. Do I still need to upgrade?+
Yes. The image optimizer flaw affects apps without those features, and Vercel advises upgrading even if you have no affected use cache functions.
Why is sharp still 0.34 after upgrading to Next.js 15.5.28?+
Next.js 15.5.28 still accepts sharp 0.34.3 or later, and no fixed 0.34 release exists. If sharp is in your own package.json or locked, run npm install sharp@^0.35.5.

Want your Next.js app patched?

I upgrade Next.js and React apps, fix what the upgrade breaks, and check the result before it goes live. See Next.js and React bug fixing. Running PHP as well? The PHP2Shell guide covers this month's PHP fixes.

Sources

Next steps

Keep reading

Need a hand with this?

Tell me what you are working on. Free scoping call, fixed quote within 48 hours.

Book a call

← All guides