Next.js ISR Cache Poisoning on a Client Site: The Revalidation Bug
A single GET /index request from an external scanner poisoned our ISR cache and took down the home page. Here is how we found the root cause and fixed it.
Author
The Incident: Home Page Went Dark on a Self-Hosted Next.js 14 Site
Last Tuesday at 14:23 IST, our monitoring stack lit up. The academy website, a Next.js 14.2.6 app running on Kubernetes in a Mumbai node, started returning 404 for the home page. Not a deploy. Not a config change. Just a 404 that refused to go away.
We use the pages router for this site. The home page lives at pages/index.tsx with export const revalidate = 60. We also have a root-level catch-all at pages/[slug].tsx with dynamicParams: true that calls notFound() for unknown slugs. This setup had been stable for months.
The Client Constraint: Pages Router, Self-Hosted on Kubernetes, No Vercel
The academy website is self-hosted. We run three pods behind an NGINX ingress on Kubernetes. No Vercel. No Edge Runtime. Just Node 18 and a lot of hope. This matters because the cache poisoning advisory from Vercel explicitly calls out that Vercel deployments are not affected Cache Poisoning · Advisory · vercel/next.js · GitHub.
The Trigger: A Single GET /index Request from an External Scanner
At 14:22:47 IST, an external scanner (user agent Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)) sent GET /index to our ingress. The request was routed to the [slug] dynamic route. Since index is not a known slug, notFound() was called. The response was a 404.
But here is the bug. Next.js used the same internal cache key for /index and /. The notFound() result for /index overwrote the ISR cache entry for /. From that moment on, every request to / returned 404. The cache entry had a stale-while-revalidate lifetime, so it persisted across all three pods.
What We Tried First and What Failed
Attempt 1: Restarting Pods Temporarily Fixed It, But It Came Back
We restarted the pods. The home page came back. Twenty minutes later, the same scanner hit /index again. The 404 returned. Restarting pods was not a fix. It was a band-aid that confirmed the issue was cache poisoning, not a code regression.
Attempt 2: Adding Cache-Control: no-store Headers at the CDN Layer
We added Cache-Control: no-store to the NGINX ingress for all routes. This stopped the CDN from caching the 404, but the ISR cache inside Next.js still held the poisoned entry. The home page stayed dark until we restarted the pods again.
Attempt 3: Blocking /index at the Ingress Level (Too Brittle
We tried blocking /index at the NGINX ingress. This worked, but it was brittle. Any future dynamic route that happened to match a real slug would also be blocked. We needed a real fix.
Root Cause: The Revalidation Bug in Next.js ISR
The issue is documented in GitHub issue #92296 GET /index with [slug] dynamic route poisons ISR cache for /, causing permanent 404 · Issue #92296 · vercel/next.js. The root cause is a cache key collision between /index and /.
The Cache Key Collision Between /index and /
In the pages router, Next.js canonicalizes /index to / internally. Both URLs resolve to the same resolvedPathname. This resolvedPathname is then used as the ISR cache key (ssgCacheKey). When the [slug] route handles /index and calls notFound(), it writes a 404 response to the cache entry for /.
The Role of Root-Level [slug] Dynamic Routes with dynamicParams: true
The [slug] route at pages/[slug].tsx has dynamicParams: true. This means it will match any path that does not match a static route. When /index is requested, it falls through to [slug]. The route calls notFound() because index is not a known slug. The 404 response is cached under the key for /, poisoning the home page.
Why This Only Affects Pages Router, Not App Router
The app router uses a different caching mechanism. It does not canonicalize /index to / in the same way. The advisory confirms that only the pages router is affected Cache Poisoning · Advisory · vercel/next.js · GitHub.
The Working Fix We Kept
Upgrading Next.js to 14.2.10 or Later
The fix is simple. Upgrade to Next.js 14.2.10 or later. The advisory lists 14.2.10 as a patched version Cache Poisoning · Advisory · vercel/next.js · GitHub.
Command: npm install next@14.2.10
npm install next@14.2.10
Verifying the Fix with curl Tests
After upgrading, we ran the reproduction steps from the issue:
curl http://localhost:3000/ # returns 200
curl http://localhost:3000/index # returns 200 (stale cache)
# Wait 2 seconds
curl http://localhost:3000/ # returns 200 (was 404 before the fix)
The home page stayed up. We also ran the scanner user agent against /index to confirm the 404 no longer poisoned the cache.
Adding a Health Check for /index to Prevent Future Regressions
We added a health check to our Kubernetes deployment that hits /index every 30 seconds. If it returns anything other than 200, the pod is restarted. This is a defensive measure, not a fix, but it gives us early warning.
readinessProbe:
httpGet:
path: /index
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
Pitfalls We Would Warn an Intern About
Never Assume /index and / Are Different Routes in ISR
In the pages router, /index and / share the same cache key. This is not a bug in your code. It is a framework behavior that can bite you.
Always Test Dynamic Routes That Call notFound()
If you have a dynamic route that calls notFound(), test it against paths that collide with static routes. /index is the most common offender.
Self-Hosted Deployments Are Not Covered by Vercel Security Patches
Vercel patches are applied automatically on Vercel. Self-hosted deployments require manual upgrades. We missed the 14.2.10 patch because we were on a quarterly upgrade cycle.
CDN Caching Can Amplify the Impact of a Single Poisoned Request
Our NGINX ingress cached the 404 for 60 seconds. This meant the home page was dark for a full minute after each poisoning attempt, even after we restarted the pods.
What We Would Do Differently Next Time
Migrate to App Router to Avoid Pages Router Vulnerabilities
The app router is not affected by this vulnerability. We are planning a migration for Q2 2026.
Implement Request-Level Logging for ISR Cache Writes
We want to log every ISR cache write, including the cache key and the response status. This would have shown us the collision immediately.
Add Automated Tests for Edge Cases Like /index Collisions
We are adding a test that sends GET /index and then checks that GET / still returns 200. This test runs in our CI pipeline.
Set Up Alerting for Sudden 404 Spikes on Critical Routes
We have a Grafana alert for 404s on /, but it was set to a 5-minute window. We are lowering it to 30 seconds.
Sources
- Cache Poisoning · Advisory · vercel/next.js · GitHub
- GET /index with [slug] dynamic route poisons ISR cache for /, causing permanent 404 · Issue #92296 · vercel/next.js
- Next.js Cache Poisoning Vulnerability (CVE-2025-49826)
- CVE-2024-46982: Next.js Cache Poisoning Vulnerability
- Postmortem: How a Next.js 15 ISR Revalidation Bug Served Stale Content to 1M Users
Sources
Related reading
Enjoyed this article?
Back to Blog


