diff --git a/lib/fs-utils.ts b/lib/fs-utils.ts index e35dfa9..2762672 100644 --- a/lib/fs-utils.ts +++ b/lib/fs-utils.ts @@ -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 { 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 { - 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)