
Analisi dettagliata di CVE-2025-61686, una vulnerabilità di path traversal nell'archiviazione di sessione su file di React Router, inclusi causa principale, scenari di attacco e risultati dell'audit del codice.
CVE ID: CVE-2025-61686
Versioni interessate: @react-router/node 7.0.0 fino a 7.9.3
Tipo di vulnerabilità: Path Traversal / Directory Traversal
La vulnerabilità risiede nella funzione getFile() e nella logica di gestione dei file associata nel file packages/react-router-node/sessions/fileStorage.ts.
Alla riga 267 di 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 || "");
}
L'ID di sessione viene estratto dal Cookie tramite il metodo cookie.parse().
Nella funzione decodeCookieValue() in packages/react-router/lib/server-runtime/cookies.ts:
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// Se sono configurati secrets, viene verificata la firma
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // Restituisce null se la verifica della firma fallisce
}
// Se non ci sono secrets (non firmato), restituisce direttamente il valore decodificato
return decodeData(value);
}
Problema chiave: Quando il cookie non è firmato (secrets è un array vuoto o non impostato), decodeCookieValue restituisce direttamente il valore decodificato del cookie, permettendo all'attaccante di controllare completamente questo valore.
In packages/react-router-node/sessions/fileStorage.ts:
export function getFile(dir: string, id: string): string {
// Divide l'ID di sessione in una directory (primi 2 byte) e un nome file
// (rimanenti 6 byte) per ridurre la probabilità di avere directory molto grandi,
return path.join(dir, id.slice(0, 4), id.slice(4));
}
Questa funzione suddivide l'ID di sessione in due parti:
id.slice(0, 4)id.slice(4)Quindi utilizza path.join() per concatenare il percorso.
Scenario di attacco: Quando si utilizza createFileSessionStorage() e il cookie non è firmato:
../../etc/passwdgetFile() elabora:
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() normalizzi il percorso, se dir è già un percorso relativo o dopo l'elaborazione, potrebbe comunque consentire un path traversal.Modalità di sfruttamento più precisa:
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwdOppure:
../../../etc/passwd (16 caratteri)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join(), potrebbe causare un path traversal.Le seguenti operazioni sui file potrebbero essere tutte interessate:
readData(id) - Durante la lettura dei dati di sessione
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - Durante l'aggiornamento dei dati di sessione
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) - Durante l'eliminazione dei dati di sessione
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
Devono essere soddisfatte contemporaneamente le seguenti condizioni:
createFileSessionStorage()secrets non impostato nella configurazione del cookie o secrets è un array vuoto)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // Se non firmato, restituisce direttamente value
→ readData(id)
→ getFile(dir, id) // Concatenazione del percorso, rischio di path traversal
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() non convalida né normalizza il parametro idpath.join() normalizzi il percorso, in casi particolari (ad esempio quando il percorso prima della concatenazione contiene già ..) potrebbe comunque consentire un path traversal