一、风险与目标风险:任意路径读取、MIME嗅探、内联脚本执行、文件名注入与目录遍历。目标:路径规范化与扩展名白名单、精确`Content-Type`与`nosniff`、安全`Content-Disposition`与UTF-8文件名。
二、路径与类型校验import path from 'path'
function normalizeSafe(root: string, rel: string): string | null {
const p = path.normalize('/' + rel).replace(/^\/+/, '')
const full = path.join(root, p)
if (!full.startsWith(path.join(root, path.sep))) return null
return full
}
const mimeMap: Record<string, string> = {
'.pdf': 'application/pdf',
'.txt': 'text/plain; charset=utf-8',
'.csv': 'text/csv; charset=utf-8',
'.zip': 'application/zip'
}
function contentTypeOf(ext: string): string | null { return mimeMap[ext.toLowerCase()] || null }
三、文件名规范化function safeFileName(name: string): string {
const s = name.replace(/[\r\n\u0000]/g, '').replace(/[/\\]/g, '_')
return s.slice(0, 200)
}
function disposition(name: string): string {
const n = safeFileName(name)
const encoded = encodeURIComponent(n)
return `attachment; filename="${n}"; filename*=UTF-8''${encoded}`
}
四、下载响应示例type Res = { setHeader: (k: string, v: string) => void; status: (n: number) => Res; end: (b?: string) => void }
function sendDownload(res: Res, full: string, downloadName: string, fs: any) {
const ext = path.extname(full)
const ct = contentTypeOf(ext)
if (!ct) return res.status(415).end('unsupported_media_type')
res.setHeader('Content-Type', ct)
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('Content-Disposition', disposition(downloadName))
res.setHeader('Cache-Control', 'private, max-age=0, no-store')
const buf = fs.readFileSync(full)
res.end(buf.toString('binary'))
}
五、整合路由与验收type Req = { query: Record<string, string | undefined> }
function handleDownload(req: Req, res: Res, fs: any) {
const root = '/var/app/files'
const rel = req.query['file'] || ''
const name = req.query['name'] || 'download'
const full = normalizeSafe(root, rel)
if (!full) return res.status(400).end('invalid_path')
sendDownload(res, full, name, fs)
}
路径规范化通过且限制在根目录;扩展名白名单有效;`Content-Type`与`nosniff`设置正确。`Content-Disposition`为`attachment`且包含UTF-8文件名;缓存策略为私有且不存储。对不支持类型返回`415`;异常路径返回`400`;审计包含下载名与相对路径。

发表评论 取消回复