
CVE ID: CVE-2025-61686
Affected Versions: @react-router/node 7.0.0 to 7.9.3
Vulnerability Type: Path Traversal / Directory Traversal
The vulnerability exists in the getFile() function and related file operation logic in the packages/react-router-node/sessions/fileStorage.ts file.
In packages/react-router/lib/server-runtime/sessions.ts, line 267:
async getSession(cookieHeader, options) {
let id = cookieHeader && (await cookie.parse(cookieHeader, options));
let data = id && (await readData(id));
return createSession(data || {}, id || "");
}
The session ID is parsed from the cookie via the cookie.parse() method.
In the decodeCookieValue() function of packages/react-router/lib/server-runtime/cookies.ts:
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// If secrets are configured, signature verification is performed
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // Returns null if signature verification fails
}
// If no secrets configured (unsigned), directly return the decoded value
return decodeData(value);
}
Key Issue: When the cookie is unsigned (secrets is an empty array or not set), decodeCookieValue directly returns the decoded cookie value, allowing an attacker to fully control this value.
In packages/react-router-node/sessions/fileStorage.ts:
export function getFile(dir: string, id: string): string {
// Divide the session id up into a directory (first 2 bytes) and filename
// (remaining 6 bytes) to reduce the chance of having very large directories,
return path.join(dir, id.slice(0, 4), id.slice(4));
}
This function splits the session ID into two parts:
id.slice(0, 4)id.slice(4)Then uses path.join() to concatenate the path.
Attack Scenario: When using createFileSessionStorage() with an unsigned cookie:
../../etc/passwdgetFile() processing:
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() normalizes paths, path traversal may still be possible if dir is already relative or after processingMore precise exploitation:
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwdOr:
../../../etc/passwd (16 characters)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join(), path traversal could occurThe following file operations are potentially affected:
readData(id) - When reading session data
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - When updating session data
async updateData(id, data, expires) {
let content = JSON.stringify({ data, expires });
let file = getFile(dir, id);
await fsp.mkdir(path.dirname(file), { recursive: true });
await fsp.writeFile(file, content, "utf-8");
}
deleteData(id) - When deleting session data
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
The following conditions must all be met:
createFileSessionStorage() methodsecrets not set or empty array in the cookie configuration)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // When unsigned, directly returns the value
→ readData(id)
→ getFile(dir, id) // Path concatenation, risk of path traversal
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() function does not validate or normalize the id parameterpath.join() normalizes paths, path traversal may still be allowed under specific circumstances (e.g., when the preceding path already contains ..)