# Next.js, cache, and chains: the stale elixir

Next.js pages-router SSR routes could be forced into SSG caching via x-now-route-matches header, letting attackers cache poisoned pageProps JSON for DoS or stored XSS.

- Payout: Research
- Severity: critical
- Program: Vercel
- Write-up by: zhero
- Disclosed: 2025-01-21
- CVE: CVE-2024-46982
- Write-up quality: 9/10
- Original: https://zhero-web-sec.github.io/research-and-things/nextjs-cache-and-chains-the-stale-elixir
- Library page: https://bugbountylibrary.com/r/blog%3Azhero-web-sec-github-io-research-and-things-nextjs-cache-and-chains-the-stale-elixir
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:research sev:critical tech:nextjs tech:nodejs technique:source-review vuln:cache-deception vuln:cache-poisoning vuln:dos vuln:xss

**Target:** Next.js pages-router SSR endpoints and /_next/data routes
**Root cause:** The isSSG check in base-server.ts treats a request as static-generation when the x-now-route-matches header is present, so SSR responses get cacheable Cache-Control (s-maxage=1, stale-while-revalidate) instead of private/no-store, and the framework's internal cache stores attacker-controlled pageProps.
**Steps:**
1. Identify a pages-router endpoint using getServerSideProps that reflects a request value (e.g. User-Agent) into pageProps.
2. Send `GET /poc?__nextDataReq=1` with header `x-now-route-matches: 1` and a malicious `User-Agent: <img src=x onerror=alert(1)>`.
3. Response is now treated as SSG and cached with `Cache-Control: s-maxage=1, stale-while-revalidate`.
4. Automate resending the poisoned request at short intervals to keep the stale cache populated.
5. Visit `/poc` without parameters; the cached JSON is served as text/html and the payload executes.
**How found:** Source review of base-server.ts internal request handling; noticed internal headers like x-now-route-matches were not stripped from external requests and influenced the isSSG conditional, then tested whether SSR responses became cacheable.
**Exploitation:** Attacker poisons the framework's internal cache by combining __nextDataReq (or the /_next/data/{buildID}/page.json route) with x-now-route-matches so an SSR response is cached. Subsequent visitors receive the attacker's pageProps JSON as text/html, yielding stored XSS (mass ATO/data theft) or DoS by replacing page content. Cache deception variant revalidates a victim's personalized response into cache.
**Escalation:** Chaining cache poisoning with reflected request data (user-agent, cookies, locale, CSRF token) turns DoS into stored XSS; later bypass chained a race condition (CVE-2025-32421).
**Insight:** Framework-internal headers used for routing/SSG decisions (x-now-route-matches, x-invoke-status, x-middleware-prefetch) are often reachable from outside; check whether they flip cacheability or content-type decisions on SSR routes, and use Accept-Encoding as a cache-key probe to test safely.

## Proof of concept (copied exactly from the original report)

**Cache poisoning request with x-now-route-matches and XSS payload**

```
GET /poc?__nextDataReq=1 HTTP/1.1
Host: localhost:3000
User-Agent: CP TO SXSS ON NEXT.JS : <img src=x onerror=alert('Palestine')>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
x-now-route-matches: 1
```

**getServerSideProps reflecting user-agent into page props**

```
export async function getServerSideProps(context: GetServerSidePropsContext) {
 const userAgent = context.req.headers['user-agent'];
 return {
 props: {
 userAgent, 
 },
 };
}
```

