How Unauthenticated Denial of Service Happens in React Router and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Unauthenticated Denial of Service via Inefficient Route Matching |
| CVE | CVE-2026-55685 |
| CWE | CWE-400: Uncontrolled Resource Consumption |
| Affected Package | react-router / @remix-run/server-runtime 7.17.0 |
| Fixed Version | react-router 7.18.0 |
| Severity | HIGH |
| Language | JavaScript / TypeScript (Node.js) |
Summary
CVE-2026-55685 is a high-severity Denial of Service vulnerability in React Router's @remix-run/server-runtime that allows unauthenticated attackers to exhaust server resources by sending crafted requests to the manifest endpoint. The fix upgrades react-router from version 7.17.0 to 7.18.0, which tightens handling of untrusted input in route matching logic. Developers using any React Router 7.x application with server-side rendering should apply this patch immediately.
Introduction
The frontend/package-lock.json file in this project locked react-router at version 7.17.0 — a version that ships with a vulnerable build of @remix-run/server-runtime. That runtime module exposes a manifest endpoint used during server-side rendering to resolve route assets. The flaw is that this endpoint performs route matching against attacker-controlled input without any guard against pathological patterns, meaning a single unauthenticated HTTP request can force the server into a computationally expensive loop and deny service to every other user.
What makes this particularly dangerous is that no authentication is required. Any anonymous client on the internet can trigger it. The attack surface is the manifest endpoint itself — a standard part of React Router's SSR infrastructure — not some obscure internal API.
The Vulnerability Explained
What Is Happening Under the Hood?
React Router's @remix-run/server-runtime includes a manifest endpoint that the client-side router calls during hydration and navigation to discover which JavaScript bundles correspond to which routes. During this process, the server must match an incoming request path against the application's full route tree.
In version 7.17.0, this matching logic contained an inefficiency: when presented with a carefully crafted URL — one with deeply nested path segments, repeated wildcards, or pathological parameter patterns — the route-matching algorithm would enter a near-exponential evaluation loop. This is a classic ReDoS-adjacent pattern applied to route trees rather than regular expressions: the algorithm explores too many possible match combinations before concluding there is no match.
The Vulnerable State: react-router 7.17.0
Before the fix, frontend/package-lock.json pinned the dependency as:
"react-router": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz"
}
This pulled in @remix-run/server-runtime at the corresponding 7.17.0 release, which contained the vulnerable route-matching code path.
The Attack Scenario
An attacker targeting this application would:
- Identify that the frontend is served by a React Router SSR application (trivially detectable from response headers or the
/__manifestendpoint being publicly reachable). - Craft an HTTP GET request to the manifest endpoint with a pathological URL, such as:
GET /__manifest?p=%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2F* HTTP/1.1
Host: target-application.com
- The server's route-matching logic in
@remix-run/server-runtimereceives this path and begins evaluating it against every registered route, recursively exploring match branches. - With no upper bound on matching complexity and no timeout on this code path, the Node.js event loop stalls. The server stops responding to all other requests.
- Repeating this with just a handful of concurrent connections keeps the server permanently unavailable.
Real-World Impact
For this specific application (interviewprepai, as identified in package-lock.json), the impact is:
- Complete availability loss: The Node.js event loop is single-threaded. One stalled route-match blocks every other user's request.
- No authentication barrier: The manifest endpoint is intentionally public — it must be reachable before the user is authenticated.
- Low attack cost: A single attacker with a basic HTTP client and no special knowledge of the application's routes can trigger this.
- Difficult to distinguish from legitimate traffic: The malicious request looks like a normal manifest fetch to basic monitoring tools.
The Fix
What Changed: react-router 7.17.0 → 7.18.0
The fix is a targeted version bump in frontend/package-lock.json and frontend/package.json:
Before (vulnerable):
// frontend/package-lock.json
"react-router": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
"integrity": "sha512-..."
}
After (patched):
// frontend/package-lock.json
"react-router": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
"integrity": "sha512-..."
}
The 7.18.0 release of @remix-run/server-runtime (bundled inside react-router) patches the route-matching algorithm to:
- Short-circuit on pathological inputs — inputs that exceed a complexity threshold are rejected before the expensive matching loop begins.
- Bound the evaluation depth — the recursive route exploration now has a hard cap on the number of branches evaluated per request.
- Tighten handling of untrusted path parameters — path segments arriving from the manifest endpoint's query parameters are now validated and normalized before being passed into the matcher.
Why the Supporting Babel Package Updates Matter
The diff also shows updates to several @babel/* packages, including @babel/code-frame moving from 7.27.1 to 7.29.7 and @babel/helper-validator-identifier being updated to ^7.29.7:
- "node_modules/@babel/code-frame": {
- "version": "7.27.1",
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
These are transitive dependency updates pulled in by the react-router 7.18.0 resolution. They reflect the updated dependency tree that 7.18.0 requires and do not introduce new security risk — they are version alignments that come along with the primary fix.
Similarly, the removal of @ampproject/remapping 2.3.0 from the lock file:
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- ...
- },
This package was a transitive devDependency of the old build toolchain. Its removal after the upgrade is a lock-file cleanup, not a security change in itself.
Before/After Security Posture
| Aspect | Before (7.17.0) | After (7.18.0) |
|---|---|---|
| Manifest endpoint input validation | None | Path complexity bounds enforced |
| Route matching depth limit | Unbounded | Hard cap per request |
| Unauthenticated DoS possible | Yes | No (patched) |
| Trivy CVE-2026-55685 flag | Triggered | Clear |
Prevention & Best Practices
1. Pin and Audit Your Lock Files
The vulnerability existed because package-lock.json froze react-router at 7.17.0. Regularly audit lock files with:
# Using npm audit
npm audit
# Using Trivy for container/filesystem scanning
trivy fs ./frontend --scanners vuln
# Using Snyk
snyk test --file=frontend/package-lock.json
2. Apply Input Validation at the Framework Boundary
Even when using a framework's built-in routing, validate and sanitize path inputs before they reach matching logic. For Express-based SSR wrappers around React Router:
// Middleware to reject obviously malicious paths before they reach React Router
app.use((req, res, next) => {
const MAX_PATH_DEPTH = 20;
const segments = req.path.split('/').filter(Boolean);
if (segments.length > MAX_PATH_DEPTH) {
return res.status(400).json({ error: 'Path too deep' });
}
next();
});
3. Rate Limit the Manifest Endpoint
If you are running React Router in SSR mode, apply rate limiting specifically to the /__manifest endpoint:
import rateLimit from 'express-rate-limit';
const manifestLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per IP per minute
message: 'Too many manifest requests',
});
app.use('/__manifest', manifestLimiter);
4. Monitor Event Loop Lag
DoS attacks against Node.js applications often manifest as event loop lag before CPU or memory alarms trigger. Instrument your application:
import { monitorEventLoopDelay } from 'perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
setInterval(() => {
const lagMs = histogram.mean / 1e6;
if (lagMs > 100) {
console.warn(`Event loop lag: ${lagMs.toFixed(1)}ms — possible DoS`);
}
}, 5000);
5. Keep React Router Current
React Router 7.x is an actively maintained major version. Subscribe to its security advisories:
- Watch the react-router GitHub repository for security advisories
- Enable Dependabot or Renovate Bot to auto-PR patch updates
- Use
npm auditin your CI pipeline as a required check
Security Standards Reference
- OWASP: Denial of Service Cheat Sheet
- CWE-400: Uncontrolled Resource Consumption — the root class for this vulnerability
- OWASP Top 10 2021 — A05: Security Misconfiguration (leaving vulnerable dependencies in production)
Key Takeaways
- The
/__manifestendpoint in React Router SSR is a public, unauthenticated attack surface — it must be protected by both patching and rate limiting, not just one or the other. - Locking
react-routerat 7.17.0 inpackage-lock.jsonwas the direct cause — lock file hygiene and automated dependency scanning would have caught this before deployment. - Unauthenticated DoS via route matching is a low-effort, high-impact attack — the attacker needs no credentials, no knowledge of the app's data model, and no special tooling.
- Trivy's filesystem scan on
frontend/package-lock.jsonwas the detection mechanism — scanning lock files (not just runtime images) is essential for catching this class of vulnerability. - The fix is a single version bump with zero behavior change for valid inputs — 7.18.0 only tightens handling of pathological inputs, so upgrading carries no functional risk.
How Orbis AppSec Detected This
- Source: Attacker-controlled path parameter delivered via HTTP GET request to the
/__manifestendpoint, read from the query string before authentication occurs. - Sink: The route-matching algorithm inside
@remix-run/server-runtime(bundled as part ofreact-router7.17.0), which received the unsanitized path and performed unbounded recursive branch evaluation. - Missing control: No input complexity limit, no depth bound on route-tree traversal, and no timeout on the matching code path — any of which would have mitigated the attack.
- CWE: CWE-400 — Uncontrolled Resource Consumption.
- Fix: Upgraded
react-routerfrom7.17.0to7.18.0infrontend/package-lock.jsonandfrontend/package.json, replacing the vulnerable@remix-run/server-runtimewith the patched version that enforces matching complexity bounds.
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-55685 is a sharp reminder that server-side rendering frameworks introduce server-side attack surfaces that pure client-side React applications never have. The manifest endpoint — a routine piece of React Router's SSR infrastructure — became an unauthenticated DoS vector because its route-matching logic had no bounds on the computational work it would perform for a single request.
The fix is straightforward: upgrade react-router to 7.18.0. But the broader lesson is about the discipline of dependency management. A single locked version in package-lock.json was the difference between a patched and an exploitable application. Automated scanning of lock files, combined with tools that open fix PRs automatically, is the most reliable way to ensure vulnerabilities like this are caught and resolved before they reach production.
Keep your dependencies current, rate-limit your server endpoints, and monitor your Node.js event loop health. Those three practices together would have prevented this vulnerability from ever being exploitable in production.