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) {
// シークレットが設定されている場合、署名を検証
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // 署名検証失敗時は null を返す
}
// シークレットが設定されていない場合(署名なし)、デコードした値をそのまま返す
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 を 2 つの部分に分割します:
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() はパスを正規化するが、特定の状況(結合前のパスに既に .. が含まれている場合など)では、パストラバーサルを許容する可能性がある