# Eclipse on Next.js: Conditioned exploitation of an intended race-condition

Next.js batcher promise dedup keyed on /_error-0 lets a race between two requests leak pageProps into a text/html response, enabling cache poisoning and stored XSS.

- Payout: Research
- Severity: medium
- Program: Vercel
- Write-up by: zhero
- Disclosed: 2025-05-06
- CVE: CVE-2025-32421
- Write-up quality: 8/10
- Original: https://zhero-web-sec.github.io/research-and-things/eclipse-on-nextjs-conditioned-exploitation-of-an-intended-race-condition
- Library page: https://bugbountylibrary.com/r/blog%3Azhero-web-sec-github-io-research-and-things-eclipse-on-nextjs-conditioned-exploitation-of-an-intended-race-condition
- Tags: area:web domain:web impact:dos impact:integrity kind:research platform:research sev:medium tech:nextjs technique:chained technique:race-window vuln:cache-poisoning vuln:race-condition vuln:xss

**Target:** Next.js pages router SSR endpoint / batcher cacheKey
**Root cause:** The batcher deduplicates in-flight promises by cacheKey; requests with x-now-route-matches and __nextDataReq both produce /_error-0 keys, so a concurrent request can receive the pageProps result that was meant to be overwritten downstream.
**Steps:**
1. Send request A to an SSR endpoint with `x-now-route-matches` header plus `__nextDataReq` URL parameter; it triggers three batcher calls: /poc-0, then two /_error-0 calls, the second /_error-0 producing pageProps then overwritten with InternalServerError.
2. Send request B to any erroring page with only `x-now-route-matches` header, also producing /_error-0.
3. Fire A and B simultaneously (single-packet attack) so A's calls reach the batcher first.
4. B's response resolves from A's shared promise, returning pageProps as text/html (500 status).
5. If error page pageProps reflect request data (e.g. via getInitialProps in _app.tsx) and a CDN caches non-JSON responses ignoring origin Cache-Control, poison the cache for stored XSS.
**How found:** While hunting BBP targets for the prior stale-elixir bug, found an asset on a patched version behaving oddly; debugged the batcher and saw fn called multiple times with /_error-0 keys, then hypothesized promise sharing could leak the overwritten pageProps.
**Exploitation:** Attacker races two requests sharing the /_error-0 batcher key; the second request inherits the first's pageProps body in a text/html response. Combined with getInitialProps reflecting request elements (cookies) on error pages and a CDN (observed Fastly) caching non-JSON responses despite private/no-store, the poisoned response is served to other users, yielding stored XSS or DoS.
**Escalation:** Chained race-condition with external CDN cache poisoning; pageProps reflection via legacy getInitialProps on _app.tsx escalates DoS to stored XSS.
**Insight:** When a framework dedups work by a coarse key like /_error-0, any two erroring requests share results — look for race windows where one path's internal body is overwritten downstream but still resolved into another caller's response.

