
Analyse détaillée de CVE-2025-61686, une vulnérabilité de traversée de chemin dans le stockage de session par fichier de React Router, incluant la cause racine, les scénarios d'attaque, et les conclusions de l'audit de code.
CVE ID: CVE-2025-61686
Versions affectées: @react-router/node 7.0.0 à 7.9.3
Type de vulnérabilité: Traversée de chemin (Path Traversal) / Dépassement de répertoire
La vulnérabilité se trouve dans la fonction getFile() et la logique associée de manipulation de fichiers dans le fichier packages/react-router-node/sessions/fileStorage.ts.
À la ligne 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 || "");
}
L'identifiant de session est extrait du cookie via la méthode cookie.parse().
Dans la fonction decodeCookieValue() de packages/react-router/lib/server-runtime/cookies.ts :
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// Si des secrets sont configurés, la signature est vérifiée
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // Retourne null si la vérification de la signature échoue
}
// Si le cookie n'est pas signé (secrets est un tableau vide ou non défini), retourne directement la valeur décodée
return decodeData(value);
}
Problème clé : Lorsque le cookie n'est pas signé (secrets est un tableau vide ou non défini), decodeCookieValue retourne directement la valeur décodée du cookie, ce qui permet à un attaquant de contrôler complètement cette valeur.
Dans packages/react-router-node/sessions/fileStorage.ts :
export function getFile(dir: string, id: string): string {
// Le session id est divisé en un répertoire (2 premiers octets) et un nom de fichier
// (6 octets restants) pour réduire la probabilité de répertoires très volumineux
return path.join(dir, id.slice(0, 4), id.slice(4));
}
Cette fonction divise l'identifiant de session en deux parties :
id.slice(0, 4)id.slice(4)Ensuite, elle utilise path.join() pour concaténer le chemin.
Scénario d'attaque : Lorsque createFileSessionStorage() est utilisé et que le cookie n'est pas signé :
../../etc/passwdgetFile() :
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() normalise le chemin, si dir est déjà un chemin relatif ou après traitement, une traversée de chemin peut encore être possible.Méthode d'exploitation plus précise :
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwdOu encore :
../../../etc/passwd (16 caractères)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join(), une traversée de chemin pourrait être possible.Les opérations de fichiers suivantes peuvent toutes être concernées :
readData(id) - Lors de la lecture des données de session
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - Lors de la mise à jour des données de session
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) - Lors de la suppression des données de session
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
Les conditions suivantes doivent être toutes remplies :
createFileSessionStorage()secrets non défini dans la configuration du cookie ou tableau secrets vide)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // Retourne directement la valeur si non signée
→ readData(id)
→ getFile(dir, id) // Concaténation de chemin, risque de traversée de chemin
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() ne valide ni ne normalise le paramètre idpath.join() normalise le chemin, dans certains cas (par exemple lorsque le chemin avant concaténation contient déjà ..), la traversée de chemin peut encore être possible