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 { 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[]> {
const dirs = await fs.readdir(RESOURCE_PATH);
const projectDirs = dirs.filter(dir => {
const dirPath = path.join(RESOURCE_PATH, dir);
// 检查是否是目录
return fs.stat(dirPath).then(stats => stats.isDirectory()).catch(() => false);
});
return projectDirs;
const projectDirs = await Promise.all(
dirs.map(async (dir) => {
const dirPath = validatePath(RESOURCE_PATH, dir);
try {
const stats = await fs.stat(dirPath);
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[]> {
const projectPath = path.join(RESOURCE_PATH, project);
const projectPath = validatePath(RESOURCE_PATH, project);
const dirs = await fs.readdir(projectPath);
const versionDirs = dirs.filter(dir => {
const dirPath = path.join(projectPath, dir);
return fs.stat(dirPath).then(stats => stats.isDirectory()).catch(() => false);
});
return versionDirs.sort();
const versionDirs = await Promise.all(
dirs.map(async (dir) => {
const dirPath = validatePath(projectPath, dir);
try {
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;
modifiedAt: Date;
}[]> {
const apksPath = path.join(RESOURCE_PATH, project, version, 'apks');
const apksPath = validatePath(RESOURCE_PATH, project, version, 'apks');
try {
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(
apkFiles.map(async (file) => {
const filePath = path.join(apksPath, file);
const filePath = validatePath(apksPath, file);
const stats = await fs.stat(filePath);
// 提取 commit id (假设文件名格式为 xxx-{commit}-xxx.apk)