CVE ID: CVE-2025-61686
影响版本: @react-router/node 7.0.0 至 7.9.3
漏洞类型: 路径遍历 (Path Traversal) / 目录穿越
漏洞存在于 packages/react-router-node/sessions/fileStorage.ts 文件中的 getFile() 函数和相关的文件操作逻辑。
在 packages/react-router/lib/server-runtime/sessions.ts 的第 267 行:
async getSession(cookieHeader, options) {
let id = cookieHeader && (await cookie.parse(cookieHeader, options));
let data = id && (await readData(id));
return createSession(data || {}, id || "");
}
Session ID 从 Cookie 中解析而来,通过 cookie.parse() 方法。
在 packages/react-router/lib/server-runtime/cookies.ts 的 decodeCookieValue() 函数中:
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// 如果配置了 secrets,会验证签名
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // 签名验证失败返回 null
}
// 如果没有配置 secrets(未签名),直接返回解码后的值
return decodeData(value);
}
关键问题:当 cookie 未签名(secrets 为空数组或未设置)时,decodeCookieValue 会直接返回解码后的 cookie 值,攻击者可以完全控制这个值。
在 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));
}
该函数将 session ID 分成两部分:
id.slice(0, 4)id.slice(4)然后使用 path.join() 拼接路径。
攻击场景:当使用 createFileSessionStorage() 且 cookie 未签名时:
../../etc/passwdgetFile() 函数处理:
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() 会规范化路径,但如果 dir 已经是相对路径或经过处理后,仍可能实现路径遍历更精确的利用方式:
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwd或者:
../../../etc/passwd(16字符)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join() 的行为,可能导致路径遍历以下文件操作都可能受影响:
readData(id) - 读取会话数据时
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - 更新会话数据时
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) - 删除会话数据时
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
必须同时满足以下条件:
createFileSessionStorage() 方法cookie 配置中未设置 secrets 或 secrets 为空数组)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // 未签名时直接返回 value
→ readData(id)
→ getFile(dir, id) // 路径拼接,存在路径遍历风险
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() 函数没有对 id 参数进行验证或规范化path.join() 会规范化路径,但在特定情况下(如拼接前路径已包含 ..)仍可能允许路径遍历