All Posts
Cloud & DevOps

Your CMS Picks Your Next.js Patch. Read Its Peer Range First.

Next.js 15.5.27 carries the September SSRF fix but sits outside Payload's peer range. For a Payload site, the only patched, supported path is Next.js 16.

9 min read

Next.js shipped a security release on 30 September that fixes a high-severity server-side request forgery in Image Optimization, in versions 16.3.8 and 15.5.27. If your site runs Payload CMS, only one of those is a version you can sensibly install. The current @payloadcms/next declares peer support for Next.js 15.4.11 up to, but not including, 15.5.0, and then for 16.3.3 and above, which leaves the 15.5 line out entirely. Next.js 15 also reaches the end of its two-year Maintenance LTS window on 21 October, so for a Payload site on Next 15 the real choice is a move to 16 this month or an unpatched 15.4. A peer range is the set of framework versions a library's authors have declared support for, and on a CMS-backed site it, not the framework's changelog, sets the real boundary of your security patch path.

Why this matters now

The Next.js September 2026 security release says the Image Optimization issue applies when an attacker-controlled, allow-listed remote URL can reach the optimizer, and that an application with no images.remotePatterns configured is not affected. Most sites that serve remote images through next/image have patterns configured, and this one does: it allows its own host. I am not claiming this site is exploitable. I am saying I should treat 15.4.11 as unpatched, because the release lists fixes only for 16.3.8 and 15.5.27. The same release also lists six medium issues and one low, including a metadata-image-route problem that affects only apps built with webpack, which is the bundler Next.js 15 uses unless you opt in to another.

The other date is the framework's own. Per the Next.js support policy, each major stays in Maintenance LTS for two years after its initial release, shipping only critical bug fixes and essential security updates. Next.js 15 was released on 21 October 2024. The policy page lists no end date for 15 itself, so 21 October 2026 is the rule applied, not a published promise. What the release note cannot tell you is that your CMS has never declared support for the version it recommends. That information lives one level down, in a package manifest.

The gate: test the patch against the peer range

I read the range from the npm manifest for @payloadcms/next, where the latest release is 3.90.2, and put it in a ten-line script next to the three candidate versions. The range moves with every Payload release and this repository is still on the 3.73 line, so read your own installed version's range with npm view @payloadcms/next@<your-version> peerDependencies.next before you trust mine.

// peer-gate.ts: which Next.js versions will my CMS accept, and which carry the fix?
import semver from "semver"

// Read yours: npm view @payloadcms/next@<your-version> peerDependencies.next
const peer =
  ">=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.3.3 <17.0.0" // @payloadcms/next 3.90.2

const candidates = [
  { version: "15.4.11", fixesSsrf: false },
  { version: "15.5.27", fixesSsrf: true },
  { version: "16.3.8", fixesSsrf: true },
]

for (const c of candidates) {
  const allowed = semver.satisfies(c.version, peer)
  const verdict = allowed && c.fixesSsrf ? "TAKE" : allowed ? "ALLOWED, UNPATCHED" : "BLOCKED BY PEER RANGE"
  console.log(c.version.padEnd(8), String(allowed).padEnd(6), String(c.fixesSsrf).padEnd(6), verdict)
}

// Output (run and verified):
// 15.4.11  true   false  ALLOWED, UNPATCHED
// 15.5.27  false  true   BLOCKED BY PEER RANGE
// 16.3.8   true   true   TAKE

The 15.5.27 row is the one that matters. It is the patch the Next.js note recommends for the 15 line, and under this peer range it is not a combination the CMS declares support for. Depending on your package manager and settings it may still install with a warning instead of an error, which is worse, because a warning is easy to scroll past. The comparison for a Payload site on Next 15 today:

  • Next.js 15.4.11: inside Payload's range, no fix for the September issues, and out of Maintenance LTS on 21 October.
  • Next.js 15.5.27: carries the fix, but outside the range. You would be running an undeclared pairing.
  • Next.js 16.3.8: Active LTS, carries the fix, and inside the range, which starts at 16.3.3.

What 16 changes in this repository

I audited this site's repository against the Next.js 16 upgrade guide. It is a static read, not a rehearsal, so it tells me where to look rather than what will pass. Four things will bite and two will not.

The first is the build. The repository has a custom webpack block in next.config.js that sets a module extension alias, the kind of block Payload sites carry so imports ending in .js resolve to TypeScript files. In Next.js 16, next build uses Turbopack by default and fails when it finds a webpack configuration. The guide offers three ways out: build with --turbopack and ignore the block, port it to Turbopack options, or opt out with --webpack. I would take the flag first because it changes one variable, and port later.

The second is linting. Next.js 16 removes next lint, and both lint scripts in package.json call it. A codemod moves them to the ESLint CLI. The third is cache invalidation. revalidateTag now takes a second argument naming a cacheLife profile, and the single-argument form is a TypeScript error. I count twelve calls across six hook files, covering pages, posts, projects, the header, the footer and redirects. These hooks are what make a publish appear on the live site, so they are the most important thing to rehearse. The choice is deliberate: with 'max' a visitor can see the old page while the new one is fetched in the background, which is a behaviour change for an editor who expects to refresh and see their edit.

The fourth is image defaults. Allowed qualities now default to 75 only, the minimum cache TTL moves from 60 seconds to four hours, and local images with query strings need an explicit pattern. Check any quality prop and any versioned image URL. The two that will not bite: there is no middleware file to rename to proxy, and the dynamic pages already await params and draftMode, so removal of synchronous access should be a no-op here, though the codemod will confirm it.

What this changes for a founder or CTO

A framework's security note is written for its users in general. Your actual patch path is the intersection of every vendor's support matrix in your stack, and the CMS usually has the narrowest one. The consequence is that the upgrade you meant to schedule next quarter now has a date on it, set by two calendars you do not control. My estimate, not a measurement, is a day or two of changes and a day of rehearsal for a site this size. What matters is that the work is on someone's plate before 21 October rather than after an incident.

A three-step pattern

  1. Gate. Read the peer range of the CMS version you have installed and test every candidate Next.js version against it with semver before anyone reads a changelog.
  2. Audit. Search the repository for custom webpack config, next lint, single-argument revalidateTag, middleware.ts, and synchronous params, cookies or headers. Count the hits and put the count in the plan.
  3. Rehearse. Deploy the branch as a preview, publish a throwaway draft, and confirm the revalidation hooks update the live page. Then check images and the admin's live preview.

My perspective

On a CMS-backed site I read the narrowest support matrix first and the framework's changelog second. The opposite habit is how a team ends up treating a vendor's patch note as an instruction when it is only a statement about the vendor's own product. I would move this site to 16.3.8 rather than look for a way to run 15.5.27 under Payload, because an undeclared pairing buys a few weeks of calendar and costs the one property a CMS exists to provide, which is a boring publish path. My position has a price, and I want it stated: I take on Turbopack as the default build and a Payload bump in the same window, and 16 is a younger line than 15.5. I accept that because 16 is the Active LTS line, the side of the support policy that will still be covered next year.

What to do before 21 October

Run the gate against your installed Payload version, bump Payload to a release whose range includes 16.3.3 or higher, and keep that bump as its own commit. Then branch and run the upgrade codemod, npx @next/codemod@canary upgrade latest, plus the async request API codemod if you carry old synchronous access. Build with the webpack flag, deploy a preview, publish a throwaway draft and watch it appear. Merge when the revalidation hooks behave, and leave Turbopack and Cache Components for later, since neither is required to be patched. If you carry the Next.js security releases yourself, my patch playbook covers the routine side. For a second pair of eyes on the sequence for your stack, my services page describes how I work as a fractional CTO, and the Freya Coach case study shows a Next.js and Vercel build I shipped.

Get the patch path in writing before the 21st

If your CMS pins a Next.js range and you are not sure which side of it you are on, book a time and we will read it together.

#Next.js#Payload CMS#Security Patching#Dependency Management#Fractional CTO