
Análise detalhada de CVE-2025-61686, uma vulnerabilidade de path traversal no armazenamento de sessão em arquivo do React Router, incluindo causa raiz, cenários de ataque e descobertas de auditoria de código.
CVE ID: CVE-2025-61686
Versões Afetadas: @react-router/node 7.0.0 a 7.9.3
Tipo de Vulnerabilidade: Path Traversal / Directory Traversal
A vulnerabilidade existe na função getFile() e na lógica de operações de arquivo relacionadas no arquivo packages/react-router-node/sessions/fileStorage.ts.
Na linha 267 de packages/react-router/lib/server-runtime/sessions.ts:
async getSession(cookieHeader, options) {
let id = cookieHeader && (await cookie.parse(cookieHeader, options));
let data = id && (await readData(id));
return createSession(data || {}, id || "");
}
O Session ID é analisado a partir do Cookie, através do método cookie.parse().
Na função decodeCookieValue() em packages/react-router/lib/server-runtime/cookies.ts:
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// Se secrets estiver configurado, a assinatura será verificada
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // Retorna null se a verificação da assinatura falhar
}
// Se não houver secrets (não assinado), retorna diretamente o valor decodificado
return decodeData(value);
}
Problema Central: Quando o cookie não está assinado (secrets é um array vazio ou não definido), decodeCookieValue retorna diretamente o valor do cookie decodificado, permitindo que o atacante controle completamente este valor.
Em packages/react-router-node/sessions/fileStorage.ts:
export function getFile(dir: string, id: string): string {
// Divide o id da sessão em um diretório (primeiros 2 bytes) e um nome de arquivo
// (6 bytes restantes) para reduzir a chance de diretórios muito grandes,
return path.join(dir, id.slice(0, 4), id.slice(4));
}
Esta função divide o Session ID em duas partes:
id.slice(0, 4)id.slice(4)Em seguida, usa path.join() para concatenar o caminho.
Cenário de Ataque: Ao usar createFileSessionStorage() com cookie não assinado:
../../etc/passwdgetFile():
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() normalize o caminho, se dir já for um caminho relativo ou após processamento, ainda pode ser possível realizar path traversalUma forma mais precisa de exploração:
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwdOu:
../../../etc/passwd (16 caracteres)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join(), pode resultar em path traversalAs seguintes operações de arquivo podem ser afetadas:
readData(id) - Ao ler dados da sessão
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - Ao atualizar dados da sessão
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) - Ao excluir dados da sessão
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
Todas as seguintes condições devem ser atendidas simultaneamente:
createFileSessionStorage()cookie não possui secrets definido ou secrets é um array vazio)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // Retorna diretamente o value quando não assinado
→ readData(id)
→ getFile(dir, id) // Concatenação de caminho, risco de path traversal
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() não valida ou normaliza o parâmetro idpath.join() normalize o caminho, em situações específicas (como quando o caminho anterior já contém ..) ainda pode permitir path traversal