/** * R29修复:路径安全工具 * * 提供路径遍历攻击防护功能 * * 常见攻击方式: * - ../../../etc/passwd * - ..%2F..%2F..%2Fetc%2Fpasswd (URL编码) * - ....//....//etc/passwd (双斜杠绕过) */ import path from 'path' /** * 检查路径是否包含路径遍历字符 * @param inputPath 待检查的路径 * @returns 是否安全 */ export function isPathSafe(inputPath: string): boolean { if (!inputPath) return false // 解码 URL 编码 let decodedPath = inputPath try { decodedPath = decodeURIComponent(inputPath) } catch { // 解码失败,使用原始路径 } // 检查常见的路径遍历模式 const dangerousPatterns = [ /\.\./, // .. /\.\.%2f/i, // URL编码的 ../ /%2e%2e/i, // 双点的URL编码 /\.\.\\/, // Windows 路径遍历 /\0/, // null字节注入 ] for (const pattern of dangerousPatterns) { if (pattern.test(decodedPath)) { console.warn(`[PathSecurity] 检测到危险路径模式: ${inputPath}`) return false } } return true } /** * 规范化并验证路径在允许的基目录内 * @param basePath 基础目录(必须是绝对路径) * @param userPath 用户提供的路径 * @returns 安全的绝对路径,如果不安全返回 null */ export function resolveSafePath(basePath: string, userPath: string): string | null { if (!basePath || !userPath) { return null } // 确保基目录是绝对路径 if (!path.isAbsolute(basePath)) { console.error('[PathSecurity] 基目录必须是绝对路径:', basePath) return null } // 先检查是否包含危险模式 if (!isPathSafe(userPath)) { return null } // 规范化路径 const normalizedBase = path.normalize(basePath) const fullPath = path.normalize(path.join(normalizedBase, userPath)) // 确保结果路径在基目录内 // 使用 startsWith 检查,但要注意边界情况 // 例如:/uploads 不应该匹配 /uploads-backup const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep if (fullPath === normalizedBase || fullPath.startsWith(baseWithSep)) { return fullPath } console.warn(`[PathSecurity] 路径越界: ${fullPath} 不在 ${normalizedBase} 内`) return null } /** * 验证文件路径是否在允许的目录列表中 * @param filePath 文件的绝对路径 * @param allowedDirs 允许的目录列表 * @returns 是否在允许的目录中 */ export function isPathInAllowedDirs(filePath: string, allowedDirs: string[]): boolean { if (!filePath || !allowedDirs || allowedDirs.length === 0) { return false } const normalizedPath = path.normalize(filePath) for (const dir of allowedDirs) { const normalizedDir = path.normalize(dir) const dirWithSep = normalizedDir.endsWith(path.sep) ? normalizedDir : normalizedDir + path.sep if (normalizedPath === normalizedDir || normalizedPath.startsWith(dirWithSep)) { return true } } return false } /** * 清理文件名,移除危险字符 * @param fileName 原始文件名 * @returns 安全的文件名 */ export function sanitizeFileName(fileName: string): string { if (!fileName) return 'unnamed' // 移除路径分隔符和其他危险字符 return fileName .replace(/[\/\\]/g, '_') // 替换路径分隔符 .replace(/\.\./g, '_') // 替换 .. .replace(/[\x00-\x1f]/g, '') // 移除控制字符 .replace(/[<>:"|?*]/g, '_') // 移除 Windows 不允许的字符 .trim() || 'unnamed' } /** * 获取允许的上传/下载目录 * 基于项目根目录 */ export function getAllowedFileDirectories(): string[] { const cwd = process.cwd() return [ path.join(cwd, 'uploads'), path.join(cwd, 'temp'), path.join(cwd, 'public'), // 添加其他允许的目录 ] }