
React Router의 파일 세션 저장소에서 경로 탐색 취약점인 CVE-2025-61686에 대한 상세 분석으로, 근본 원인, 공격 시나리오 및 코드 감사 결과를 포함합니다.
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.parse() 메서드를 통해 Cookie에서 파싱됩니다.
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()의 정규화에 의존: path.join()이 경로를 정규화하지만, 특정 상황(예: 결합 전 경로에 이미 ..가 포함된 경우)에서는 여전히 경로 순회를 허용할 수 있습니다.