CVE-2025-61686

Path Traversal in React Router: The Cookie That Ate Your Filesystem

Alon Barad
Alon Barad
Software Engineer

Jan 15, 2026·5 min read

Executive Summary (TL;DR)

If you are using `@react-router/node` or `@remix-run/node` with file-based session storage and have not configured signed cookies (secrets), your application trusts the client-provided session ID implicitly. Attackers can craft malicious cookie values containing directory traversal sequences (`../../`) to escape the session directory. This grants them the ability to delete files (DoS) or overwrite them with JSON data, leading to potential data corruption or application compromise.

A critical path traversal vulnerability in React Router and Remix's file-based session storage allows remote attackers to delete or overwrite arbitrary files on the server by manipulating unsigned session cookies.

The Hook: Trust Issues

In the world of web development, we have a golden rule: Never trust the client. The client is a liar. The client is malicious. The client is actively trying to set your server on fire. Yet, time and time again, we see frameworks taking input directly from a request and handing it over to a sensitive system function without a second thought.

CVE-2025-61686 is a classic example of this misplaced trust. It affects the server-side runtimes of React Router (v7) and Remix (v2). Specifically, it targets the createFileSessionStorage utility. This utility is designed to store user session data as JSON files on the server's disk—a simple, effective solution for apps that don't want to spin up a Redis instance just to remember who is logged in.

But here is the catch: to find the right file on the disk, the server needs an identifier. Where does it get that identifier? From the session cookie. And what happens if we take that cookie and feed it directly into a file path resolver? We get a one-way ticket to Path Traversal City.

The Flaw: A logic hole in `getFile`

The vulnerability resides in a utility function called getFile within packages/react-router-node/sessions/fileStorage.ts. Its job is simple: take a directory path (where sessions live) and a session ID, then combine them to find the specific session file.

In a secure implementation, the code would verify that the session ID looks like a session ID—usually a random string of alphanumeric characters. However, prior to version 7.9.4, getFile was remarkably optimistic. It assumed the ID provided was safe.

When the server receives a request, it parses the cookies. If you are using unsigned cookies (which is the default if you don't provide a secrets array), the server reads the raw value of the cookie and treats it as the session ID. It then passes this ID to Node's path.join.

[!NOTE] path.join in Node.js (and similar runtimes) will resolve relative segments. If you join /sessions with ../../etc/passwd, the result is /etc/passwd.

Because there was no validation, the application effectively gave the internet read/write/delete access to any file the web server process had permission to touch, purely based on the cookie header.

The Code: The Smoking Gun

Let's look at the code. It is almost painful in its simplicity. Here is the vulnerable logic from the @react-router/node package:

// VULNERABLE CODE (Simplified)
import path from "node:path";
 
export function getFile(dir: string, id: string): string {
  // Divides the ID for directory sharding (e.g., /ab/cdef123)
  return path.join(dir, id.slice(0, 2), id.slice(2));
}

If I send a session ID of ../../../../target, the slice operations just break it up slightly, but path.join stitches it back together and normalizes the traversal. The result escapes the dir sandbox completely.

Here is the fix introduced in version 7.9.4. The maintainers added a regex check to enforce that the ID is exactly 16 hexadecimal characters—a standard format for their generated IDs.

// PATCHED CODE
export function getFile(dir: string, id: string): string | null {
  // Whitelist: strict hex validation
  if (!/^[0-9a-f]{16}$/i.test(id)) {
    return null;
  }
  return path.join(dir, id.slice(0, 2), id.slice(2));
}

This regex /^[0-9a-f]{16}$/i kills the attack dead. You can't fit .. or / into a hex-only string.

The Exploit: Deleting Production Data

So, how do we weaponize this? We need a target application using createFileSessionStorage without cookie signing.

Scenario 1: Arbitrary File Deletion (DoS) The session storage API includes a destroySession method, typically called during logout. This method maps the session ID to a file and calls fs.unlink (delete) on it.

  1. Recon: We inspect the app and see a __session cookie. It's base64 encoded but clearly just a JSON object or a string, not signed with a cryptographic signature (like s:timestamp.signature).
  2. Attack: We send a request to the logout endpoint.
    POST /logout HTTP/1.1
    Cookie: __session=../../../../var/www/html/index.html
  3. Result: The server resolves the path to the application's index.html and deletes it. The site is now broken. We can delete config files, logs, or even source code files.

Scenario 2: Arbitrary File Write (Data Corruption) When a session is created or updated, the server calls writeFile.

  1. Attack: We trigger a login or an action that sets session data.
    POST /login HTTP/1.1
    Cookie: __session=../../../../app/config.json
  2. Result: The server writes the session data (JSON) to config.json.
    • Limitation: We cannot write arbitrary bytes (like a binary executable) or raw PHP code easily, because the content is serialized via JSON.stringify. The file content will look like {"data": {...}, "expires": ...}.
    • Impact: However, if we overwrite a configuration file that expects a specific JSON structure, we crash the app. If the app blindly reads a JSON config, we might be able to inject malicious configuration values (like changing an API URL to our own server).

The Fix: Mitigation & Defense

The remediation path is straightforward but critical.

1. Patch Immediately Update your packages. If you are on the react-router ecosystem, you need @react-router/node version 7.9.4 or higher. If you are on Remix, you need @remix-run/node or @remix-run/deno version 2.17.2.

2. Enable Signed Cookies This vulnerability relies on the server trusting the client's cookie value. If you sign your cookies, the server will cryptographically verify them before attempting to use the ID.

// DO THIS:
const storage = createFileSessionStorage({
  cookie: {
    name: "__session",
    // The 'secrets' array enables signing
    secrets: ["s3cr3t_k3y_from_env_var"],
    secure: true
  },
  dir: "./sessions",
});

If a user tries to tamper with a signed cookie (e.g., changing the ID to ../../passwd), the signature verification will fail, and the server will ignore the invalid session ID entirely, preventing the path traversal logic from ever running.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
EPSS Probability
0.06%
Top 81% most exploited

Affected Systems

@react-router/node < 7.9.4@remix-run/node < 2.17.2@remix-run/deno < 2.17.2

Affected Versions Detail

Product
Affected Versions
Fixed Version
@react-router/node
Remix/React Router
7.0.0 - 7.9.37.9.4
@remix-run/node
Remix
< 2.17.22.17.2
@remix-run/deno
Remix
< 2.17.22.17.2
AttributeDetail
CWE IDCWE-22 (Path Traversal)
CVSS v3.19.1 (Critical)
Attack VectorNetwork (Cookies)
ImpactIntegrity (H), Availability (H)
Exploit StatusPoC Available
EPSS Score0.00061
CWE-22
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Vulnerability Timeline

Fix committed to repository
2025-10-06
CVE Published & Patch Released
2026-01-10

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.