Misconfigured caching of Next.js ISR/SSR responses allows attackers to poison cached pages with blank or malicious content, causing DoS for all users. [CWE-444 · A05:2021]
Impact: HIGH (denial of service or content injection affecting all users via poisoned cache)
Next.js has had multiple cache poisoning vulnerabilities where a single crafted request could poison the cache for an ISR or SSR page, serving blank or malicious content to all subsequent visitors:
s-maxage=1 cache headers, allowing upstream CDNs to cache and serve attacker-influenced responses. Affected 13.5.1–13.5.6 and 14.0.0–14.2.9.Even on patched versions, cache misconfiguration remains a risk. Understand what's cached, for how long, and who can influence it.
Incorrect (dangerous cache patterns):
// ❌ Long revalidation on user-specific content
// app/dashboard/page.tsx
export const revalidate = 3600 // Caches for 1 hour — but content varies by user!
export default async function Dashboard() {
const session = await auth()
const data = await getUserData(session.user.id)
return <DashboardView data={data} /> // User A sees User B's cached dashboard
}// ❌ Setting permissive cache headers on dynamic API routes
// app/api/user/route.ts
export async function GET(request: NextRequest) {
const user = await getCurrentUser(request)
return NextResponse.json(user, {
headers: {
'Cache-Control': 'public, s-maxage=60', // CDN caches user-specific data!
},
})
}// ❌ ISR page that renders differently based on cookies/headers
// app/pricing/page.tsx
export const revalidate = 600
export default async function Pricing() {
const country = headers().get('x-country') // Varies by request
const prices = await getPrices(country) // But ISR caches one version!
return <PriceTable prices={prices} />
}Correct (safe caching patterns):
// ✅ Never cache user-specific content with ISR
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // No caching
export default async function Dashboard() {
const session = await auth()
const data = await getUserData(session.user.id)
return <DashboardView data={data} />
}// ✅ Private cache headers for user-specific API responses
// app/api/user/route.ts
export async function GET(request: NextRequest) {
const user = await getCurrentUser(request)
return NextResponse.json(user, {
headers: {
'Cache-Control': 'private, no-store', // Never cached by CDN
},
})
}// ✅ Use generateStaticParams for known variants, not request-based branching
// app/pricing/[country]/page.tsx
export async function generateStaticParams() {
return [{ country: 'us' }, { country: 'eu' }, { country: 'uk' }]
}
export default async function Pricing({ params }: { params: { country: string } }) {
const prices = await getPrices(params.country)
return <PriceTable prices={prices} />
}Cache safety checklist:
revalidate on pages that read cookies, headers, or session data — use dynamic = 'force-dynamic' insteadCache-Control: private, no-store on API responses containing user-specific dataVary headersDetection hints:
# Find ISR pages that also read cookies or headers (likely misconfigured)
grep -rn "revalidate" src/app --include="*.ts" --include="*.tsx" -l | \
xargs grep -l "cookies\|headers\|auth\|session"
# Find API routes setting public cache headers
grep -rn "s-maxage\|public.*max-age" src/app/api --include="*.ts"Reference: CVE-2025-49826 · CVE-2024-46982 · CWE-444: Inconsistent Interpretation of HTTP Requests
denial of service or content injection affecting all users via poisoned cache
BeforeMerge scans your pull requests against this rule and 7+ others. Get actionable feedback before code ships.