- Expand batch upload list area and optimize preview image layout - Improve upload speed tracking and display (server-side & client-side) - Reduce network detection log noise - Optimize Docker build process (caching, env validation) - Add shadow environment deployment support
148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
/**
|
||
* 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'),
|
||
// 添加其他允许的目录
|
||
]
|
||
}
|