· ·

React2Shell and Your Sitecore Head: Are You on the App Router?

Alex Lentz

Title card: React2Shell and Your Sitecore Head: Are You on the App Router? Security post on Sitecore Saga.

Well, that was a fun Wednesday.

On December 3rd the React team disclosed CVE-2025-55182 – a remote code execution bug in the React Server Components protocol. Next.js published its own advisory the same day (CVE-2025-66478) for apps using the App Router. It’s rated CVSS 10.0, people are already calling it “React2Shell”, and by Thursday public exploits were out there.

I spent most of yesterday going through every head app our team touches, and the question that sorted them fastest wasn’t “what version of Next are you on?” It was “are you on the App Router?” So that’s what this post is about.

The short version

From the Next.js advisory:

  • Affected: apps using React Server Components with the App Router on Next.js 15.x, 16.x, and 14.3.0-canary.77+ canaries
  • Not affected: Next.js 13.x, 14.x stable, Pages Router apps, and the Edge Runtime
  • Fixed in: 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7 and 16.0.7 (pick the one for your release line)

There’s no config flag or workaround. You patch.

Where this lands for Sitecore folks

Here’s why the App Router question matters so much for us. If you’ve been building on JSS or Content SDK 1.x for XM Cloud (sorry – SitecoreAI, still getting used to that), your head app is most likely on the Pages Router. That’s what the starters have shipped with for years, and App Router support in Content SDK only landed as a beta in 1.2 at the end of October.

So a lot of Sitecore head apps are going to come back clean. Don’t stop there though. The ones I’d look at twice:

  1. Anything you started on the Content SDK 1.2 App Router beta. That’s the whole point of the beta – and it’s exactly the code path this bug lives in.
  2. Your Marketplace apps. This is the one I think people will miss. Marketplace apps are just Next.js (or Vite) apps, and the Next.js ones are typically App Router on Next 15. They run in an iframe inside SitecoreAI, but they’re still a publicly reachable Next.js server sitting on Vercel or wherever you deployed them. “It’s only a little custom field app” is not a security boundary.
  3. The side projects. The internal tools, the POC from the last hackathon, the “temporary” preview site. If it has an app/ folder and it’s on the internet, check it.

A quick check you can run against every repo

Eyeballing package.json isn’t enough. A ^15.5.0 range tells you nothing about what actually got installed, and the App Router question depends on the folder structure, not the package. So I wrote a tiny Node script that looks at what’s actually in node_modules and whether there’s an app/ directory:

// check-next.mjs - is this head app exposed to React2Shell (CVE-2025-66478)?
// Usage: node check-next.mjs [path-to-app]   (run after npm install)
import { existsSync, readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { join, resolve } from 'node:path';

const appDir = resolve(process.argv[2] ?? '.');
const require = createRequire(join(appDir, 'package.json'));

// First patched release per line, from the Next.js advisory (Dec 3, 2025)
const PATCHED = {
  '15.0': '15.0.5', '15.1': '15.1.9', '15.2': '15.2.6', '15.3': '15.3.6',
  '15.4': '15.4.8', '15.5': '15.5.7', '16.0': '16.0.7',
};

const parse = (v) => v.split('-')[0].split('.').map(Number);
const lt = (a, b) => {
  const [x, y] = [parse(a), parse(b)];
  for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
  return false;
};

function installed(pkg) {
  try {
    return JSON.parse(readFileSync(require.resolve(`${pkg}/package.json`), 'utf8')).version;
  } catch {
    return null;
  }
}

const next = installed('next');
if (!next) {
  console.log('next is not installed here - run npm install first.');
  process.exit(2);
}

// App Router lives in app/ or src/app/. Pages Router-only apps are not affected.
const usesAppRouter = ['app', 'src/app'].some((d) => existsSync(join(appDir, d)));
const [major, minor] = parse(next);
const line = `${major}.${minor}`;
const canary = next.includes('canary');

let verdict;
if (!usesAppRouter) {
  verdict = 'OK - no app/ directory found (Pages Router only).';
} else if (major < 15 && !canary) {
  verdict = 'OK - Next.js 14.x stable and older are not affected.';
} else if (canary) {
  verdict = 'CHECK - canary build. Compare against the advisory by hand.';
} else if (PATCHED[line] && lt(next, PATCHED[line])) {
  verdict = `PATCH NOW - upgrade to ${PATCHED[line]} or later in the ${line} line.`;
} else if (PATCHED[line]) {
  verdict = 'OK - at or above the first patched release for this line.';
} else {
  verdict = 'CHECK - version line not in the advisory table.';
}

console.log(`app:        ${appDir}`);
console.log(`next:       ${next}`);
console.log(`react:      ${installed('react') ?? 'n/a'}`);
console.log(`app router: ${usesAppRouter ? 'yes' : 'no'}`);
console.log(`verdict:    ${verdict}`);
process.exit(verdict.startsWith('PATCH') ? 1 : 0);

Output on an unpatched App Router app looks like this:

app:        C:\src\my-marketplace-app
next:       15.5.6
react:      19.1.0
app router: yes
verdict:    PATCH NOW - upgrade to 15.5.7 or later in the 15.5 line.

It exits with 1 when something needs patching, so you can drop it into a pipeline step and fail the build:

# Azure DevOps / GitHub Actions - same idea either way
- script: |
    npm ci
    node ./tools/check-next.mjs .
  displayName: React2Shell check

Got a monorepo or a pile of client repos in one folder? Loop it:

Get-ChildItem C:\src -Directory | ForEach-Object {
    if (Test-Path (Join-Path $_.FullName 'node_modules\next')) {
        node .\check-next.mjs $_.FullName
        ''
    }
}

A few honest caveats: it’s a triage tool, not a scanner. It only knows about the versions in the Dec 3 advisory, and it assumes the standard app/ or src/app/ location. If it says CHECK, go read the advisory.

Patching

For most Sitecore head apps this is a patch bump inside your current line, so it’s low drama:

# stay in your release line - e.g. 15.5.x
npm install next@15.5.7
npm ls next react react-dom   # make sure nothing else is pinning an old copy

Then the usual: build, smoke test your editing host, deploy. Do the editing host too, not just the public site. It runs the same code.

If you’re on Vercel, they pushed WAF rules to every project to block known exploit patterns. That’s a nice seatbelt, not a fix – Vercel themselves say not to rely on it. Patch anyway. And if your app was exposed and unpatched for any amount of time, rotate the secrets it has access to. For a Sitecore head that usually means your Edge context ID / API keys, your preview secret, and anything else in the project’s environment variables.

Wrapping up

The good news for a lot of the Sitecore world is that the Pages Router is boring, and boring was a feature this week. The bad news is that the newest stuff – App Router builds and Marketplace apps – is exactly where this one lives.

Remember to:

  • Check every head app, editing host, and Marketplace app – not just the main site
  • Look at what’s actually installed, not what package.json allows
  • Patch within your release line and redeploy
  • Rotate secrets on anything that sat exposed

Keep an eye on the advisories for follow-ups too. Big bugs like this tend to get people looking hard at the same code, so always take the latest patch in your line rather than the minimum.

Resources

Alex Lentz

Alex Lentz

Senior Solutions Architect at Americaneagle.com and two-time Sitecore Technology MVP. Building on Sitecore since 2013, mostly SitecoreAI and headless Next.js these days.

LinkedIn    GitHub

Keep reading