fix: address race conditions and add path validation

- Fixed race condition in getProjects() by using Promise.all with proper async/await
- Fixed race condition in getVersions() by using Promise.all with proper async/await
- Fixed race condition in getApks() by ensuring Promise.all wraps the async map
- Added validatePath() helper function to prevent path traversal attacks
- Applied path validation to all file system operations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 01:54:07 +08:00
parent 5e00e36e57
commit 2a883c23f9

View File

@@ -3,30 +3,55 @@ import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { RESOURCE_PATH } from './constants'; import { RESOURCE_PATH } from './constants';
/**
* Validate that a path doesn't escape the base directory (prevents path traversal)
*/
function validatePath(basePath: string, ...segments: string[]): string {
const fullPath = path.resolve(basePath, ...segments);
const normalizedBase = path.resolve(basePath);
if (!fullPath.startsWith(normalizedBase)) {
throw new Error('Invalid path: path traversal detected');
}
return fullPath;
}
/** /**
* 获取所有项目目录 * 获取所有项目目录
*/ */
export async function getProjects(): Promise<string[]> { export async function getProjects(): Promise<string[]> {
const dirs = await fs.readdir(RESOURCE_PATH); const dirs = await fs.readdir(RESOURCE_PATH);
const projectDirs = dirs.filter(dir => { const projectDirs = await Promise.all(
const dirPath = path.join(RESOURCE_PATH, dir); dirs.map(async (dir) => {
// 检查是否是目录 const dirPath = validatePath(RESOURCE_PATH, dir);
return fs.stat(dirPath).then(stats => stats.isDirectory()).catch(() => false); try {
}); const stats = await fs.stat(dirPath);
return projectDirs; return stats.isDirectory() ? dir : null;
} catch {
return null;
}
})
);
return projectDirs.filter((dir): dir is string => dir !== null);
} }
/** /**
* 获取指定项目的所有版本目录 * 获取指定项目的所有版本目录
*/ */
export async function getVersions(project: string): Promise<string[]> { export async function getVersions(project: string): Promise<string[]> {
const projectPath = path.join(RESOURCE_PATH, project); const projectPath = validatePath(RESOURCE_PATH, project);
const dirs = await fs.readdir(projectPath); const dirs = await fs.readdir(projectPath);
const versionDirs = dirs.filter(dir => { const versionDirs = await Promise.all(
const dirPath = path.join(projectPath, dir); dirs.map(async (dir) => {
return fs.stat(dirPath).then(stats => stats.isDirectory()).catch(() => false); const dirPath = validatePath(projectPath, dir);
}); try {
return versionDirs.sort(); const stats = await fs.stat(dirPath);
return stats.isDirectory() ? dir : null;
} catch {
return null;
}
})
);
return versionDirs.filter((dir): dir is string => dir !== null).sort();
} }
/** /**
@@ -38,7 +63,7 @@ export async function getApks(project: string, version: string, commitId?: strin
size: number; size: number;
modifiedAt: Date; modifiedAt: Date;
}[]> { }[]> {
const apksPath = path.join(RESOURCE_PATH, project, version, 'apks'); const apksPath = validatePath(RESOURCE_PATH, project, version, 'apks');
try { try {
const files = await fs.readdir(apksPath); const files = await fs.readdir(apksPath);
@@ -46,7 +71,7 @@ export async function getApks(project: string, version: string, commitId?: strin
const apks = await Promise.all( const apks = await Promise.all(
apkFiles.map(async (file) => { apkFiles.map(async (file) => {
const filePath = path.join(apksPath, file); const filePath = validatePath(apksPath, file);
const stats = await fs.stat(filePath); const stats = await fs.stat(filePath);
// 提取 commit id (假设文件名格式为 xxx-{commit}-xxx.apk) // 提取 commit id (假设文件名格式为 xxx-{commit}-xxx.apk)