/** * R29 测试:路径安全工具 * * 验证: * 1. 路径遍历攻击检测 * 2. 路径边界检查 * 3. 文件名清理 */ import path from 'path' import { isPathSafe, resolveSafePath, isPathInAllowedDirs, sanitizeFileName, getAllowedFileDirectories, normalizeSafeFileName, resolveStrictChildPath, } from '../path-security' describe('R29: 路径安全工具', () => { describe('isPathSafe - 路径遍历检测', () => { it('应该拒绝包含 .. 的路径', () => { expect(isPathSafe('../etc/passwd')).toBe(false) expect(isPathSafe('../../etc/passwd')).toBe(false) expect(isPathSafe('uploads/../../../etc/passwd')).toBe(false) }) it('应该拒绝 URL 编码的遍历路径', () => { expect(isPathSafe('%2e%2e/etc/passwd')).toBe(false) expect(isPathSafe('..%2fetc%2fpasswd')).toBe(false) }) it('应该拒绝 Windows 风格的遍历路径', () => { expect(isPathSafe('..\\Windows\\System32')).toBe(false) }) it('应该拒绝 null 字节注入', () => { expect(isPathSafe('file.txt\x00.exe')).toBe(false) }) it('应该接受正常路径', () => { expect(isPathSafe('uploads/file.zip')).toBe(true) expect(isPathSafe('uploads/2026/01/file.zip')).toBe(true) expect(isPathSafe('/absolute/path/file.zip')).toBe(true) }) it('应该拒绝空路径', () => { expect(isPathSafe('')).toBe(false) }) }) describe('resolveSafePath - 安全路径解析', () => { const basePath = '/var/www/uploads' it('应该解析在基目录内的路径', () => { const result = resolveSafePath(basePath, 'file.zip') expect(result).toBe(path.normalize('/var/www/uploads/file.zip')) }) it('应该解析子目录路径', () => { const result = resolveSafePath(basePath, '2026/01/file.zip') expect(result).toBe(path.normalize('/var/www/uploads/2026/01/file.zip')) }) it('应该拒绝越界路径', () => { const result = resolveSafePath(basePath, '../etc/passwd') expect(result).toBeNull() }) it('应该拒绝空参数', () => { expect(resolveSafePath('', 'file.zip')).toBeNull() expect(resolveSafePath(basePath, '')).toBeNull() }) it('应该拒绝相对基目录', () => { const result = resolveSafePath('uploads', 'file.zip') expect(result).toBeNull() }) }) describe('isPathInAllowedDirs - 目录边界检查', () => { const allowedDirs = ['/var/www/uploads', '/var/www/temp'] it('应该接受允许目录内的路径', () => { expect(isPathInAllowedDirs('/var/www/uploads/file.zip', allowedDirs)).toBe(true) expect(isPathInAllowedDirs('/var/www/temp/file.zip', allowedDirs)).toBe(true) }) it('应该接受子目录路径', () => { expect(isPathInAllowedDirs('/var/www/uploads/2026/01/file.zip', allowedDirs)).toBe(true) }) it('应该拒绝不在允许目录的路径', () => { expect(isPathInAllowedDirs('/etc/passwd', allowedDirs)).toBe(false) expect(isPathInAllowedDirs('/var/www/other/file.zip', allowedDirs)).toBe(false) }) it('应该拒绝前缀匹配但不是子目录的路径', () => { // /var/www/uploads-backup 不应该匹配 /var/www/uploads expect(isPathInAllowedDirs('/var/www/uploads-backup/file.zip', allowedDirs)).toBe(false) }) it('应该拒绝空参数', () => { expect(isPathInAllowedDirs('', allowedDirs)).toBe(false) expect(isPathInAllowedDirs('/var/www/uploads/file.zip', [])).toBe(false) }) }) describe('sanitizeFileName - 文件名清理', () => { it('应该移除路径分隔符', () => { expect(sanitizeFileName('path/to/file.txt')).toBe('path_to_file.txt') expect(sanitizeFileName('path\\to\\file.txt')).toBe('path_to_file.txt') }) it('应该移除路径遍历字符', () => { expect(sanitizeFileName('../file.txt')).toBe('__file.txt') expect(sanitizeFileName('../../file.txt')).toBe('____file.txt') }) it('应该移除 Windows 不允许的字符', () => { expect(sanitizeFileName('file<>:"|?*.txt')).toBe('file_______.txt') }) it('应该处理空文件名', () => { expect(sanitizeFileName('')).toBe('unnamed') }) it('应该保留正常文件名', () => { expect(sanitizeFileName('normal_file-name.txt')).toBe('normal_file-name.txt') expect(sanitizeFileName('中文文件名.zip')).toBe('中文文件名.zip') }) }) describe('getAllowedFileDirectories - 获取允许的目录', () => { it('应该返回目录列表', () => { const dirs = getAllowedFileDirectories() expect(Array.isArray(dirs)).toBe(true) expect(dirs.length).toBeGreaterThan(0) }) it('应该包含 uploads 目录', () => { const dirs = getAllowedFileDirectories() const hasUploads = dirs.some(dir => dir.includes('uploads')) expect(hasUploads).toBe(true) }) }) describe('normalizeSafeFileName - 上传叶子文件名校验', () => { it.each([ '../outside.psd', '..\\outside.psd', '/app/server.js', 'C:\\Windows\\system.ini', '\\\\server\\share\\outside.psd', '%2e%2e%2foutside.psd', '%252e%252e%255coutside.psd', '%25252e%25252e%25252foutside.psd', 'folder/name.psd', 'bad\u0000name.psd', ])('应该拒绝路径形式的文件名:%s', (fileName) => { expect(normalizeSafeFileName(fileName)).toBeNull() }) it('应该保留合法中文文件名并执行 trim', () => { expect(normalizeSafeFileName(' 中文源文件.psb ')).toBe('中文源文件.psb') }) it('应该按 UTF-8 字节数拒绝超长文件名', () => { expect(normalizeSafeFileName(`${'文'.repeat(90)}.psd`)).toBeNull() }) }) describe('resolveStrictChildPath - 严格子路径解析', () => { const root = path.resolve('safe-upload-root') it('应该只返回根目录下的严格子项', () => { expect(resolveStrictChildPath(root, 'assembled', 'file.psd')).toBe( path.join(root, 'assembled', 'file.psd'), ) }) it('应该拒绝根目录本身和越界路径', () => { expect(resolveStrictChildPath(root, '.')).toBeNull() expect(resolveStrictChildPath(root, '..', 'outside.psd')).toBeNull() expect(resolveStrictChildPath(root, path.parse(root).root, 'outside.psd')).toBeNull() expect(resolveStrictChildPath(root, 'C:\\Windows\\system.ini')).toBeNull() expect(resolveStrictChildPath(root, '\\\\server\\share\\outside.psd')).toBeNull() }) }) })