人人都会AI编程

21.1 常见 Web 攻击与防御

更新时间:2026-07-11

在 Web 应用中,文件上传和资源路径处理是两个极易出现安全漏洞的环节。由于 Node.js 服务端需要处理来自客户端的文件流和路径参数,如果缺乏严格的校验和过滤,攻击者可以利用这些漏洞上传恶意脚本、覆盖系统文件、甚至获取服务器控制权。本节将分别拆解这两种漏洞的原理,并给出 Node.js 中具体有效的防护方案。

一、文件上传漏洞防护

漏洞原理

文件上传漏洞通常源于服务端对用户上传的文件“过度信任”,常见攻击手法包括:

  • 上传可执行脚本:攻击者将 .php.jsp.js 等文件伪装成图片上传,之后通过 URL 直接访问该文件,从而在服务器上执行任意代码。
  • 任意覆盖关键文件:如果上传时未限制存储路径,攻击者可能通过 ../ 路径回溯,把文件存放到 /etc/cron.d/~/.ssh/authorized_keys 等系统高危目录,造成更大破坏。
  • 大文件或畸形文件导致拒绝服务:一个无限大的 ZIP 包或恶意构造的压缩文件可能耗尽磁盘空间或内存,拖垮服务。
  • 跨站脚本(XSS):允许上传 HTML 或 SVG 文件,且服务端返回这些文件时不设置正确的 Content-Type 或 Content-Disposition,可能导致浏览器执行其中的脚本。

Node.js 中的防护方案

1. 校验文件类型,不信任 MIME

客户端上传的 Content-Type 完全由请求方控制,攻击者可以任意伪造。因此,必须通过服务端逻辑校验文件的实际内容。常见方法有两种:

  • 文件魔数检查:每种文件类型的前几个字节通常有固定特征(例如 JPEG 以 FF D8 FF 开头,PNG 以 89 50 4E 47 开头)。可以使用 file-type 这个库来读取 Buffer 头部字节进行判断。
const fileType = require('file-type');

const buffer = fs.readFileSync(uploadedFilePath);
const type = await fileType.fromBuffer(buffer);

if (!type || !['jpg', 'png', 'gif'].includes(type.ext)) {
  throw new Error('不支持的文件类型');
}
  • 白名单校验 MIME(辅助手段):仅在文件类型确认后,再核对 MIME 是否在白名单内,但不能作为唯一防线。

2. 使用安全的存储方案

避免直接将文件存储在 Web 应用的静态资源目录下,因为你永远不希望用户上传的文件能被服务器解释执行。建议:

  • 将所有上传文件存放于应用根目录之外,例如 /var/uploads/,而非 ./public/uploads/
  • 如果必须提供 HTTP 访问,通过独立的路由读取并转存,并在响应中强制设置 Content-Disposition: attachment 和正确的 Content-Type,防止浏览器解析。
// 安全读取上传文件示例
app.get('/uploads/:filename', (req, res) => {
  const safeName = path.basename(req.params.filename); // 避免路径回溯
  const filePath = path.join(UPLOAD_DIR, safeName);

  res.setHeader('Content-Disposition', 'attachment');
  res.setHeader('Content-Type', 'application/octet-stream');
  fs.createReadStream(filePath).pipe(res);
});

3. 重命名文件,杜绝用户输入作为文件名

永远不要直接使用用户提供的原始文件名,因为它可能包含 ../、特殊符号或超长字符串。典型的做法是使用 UUID 或时间戳 + 随机字符串作为存储名称,原始文件名仅作元数据存入数据库。

const { v4: uuidv4 } = require('uuid');
const ext = path.extname(originalName); // 保留扩展名但也可不保留
const safeName = uuidv4() + ext;

4. 限制文件大小和数量

利用 multer 等中间件设置 limits.fileSize,并在应用层捕获超出大小的错误。

const multer = require('multer');
const upload = multer({
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});

同时应限制单用户短时间内上传的次数,防止恶意占用磁盘 I/O。

5. 审计和扫描恶意内容

对于图片文件,可以使用 sharp 等库重新编码,去除内嵌的恶意代码;对于文档类型,可在有条件时对接 ClamAV 等反病毒引擎。但前者是更轻量的生产手段。

// 使用 sharp 重置图片,移除非图像数据
const sharp = require('sharp');
await sharp(inputPath).resize(1024).jpeg({ quality: 80 }).toFile(outputPath);

二、路径遍历漏洞防护

漏洞原理

路径遍历(Path Traversal),也称为目录穿越,是指攻击者通过构造包含 ../ 或绝对路径的特殊字符串,访问或操作本无权访问的文件目录。典型场景是应用根据用户参数拼接文件路径时,未过滤非法符号:

GET /download?file=../../../etc/passwd

如果后端代码直接将 req.query.file 拼接到基础目录后,攻击者就能读取服务器上的任意文件。

Node.js 中许多与文件交互的模块(fspathsend)如果使用不当,都会暴露此漏洞。

防护方案

1. 宁可“拒绝”,不要“修复”

核心原则:对用户输入的任何路径片段,都要通过与白名单比对或强制规范化后验证,发现异常直接拒绝,而不要尝试移除 ../ 字符。因为攻击者的编码绕过手段多样(%2e%2e%2f、双写等),手动过滤极易遗漏。

2. 使用 path.basename() 截取文件名

当你只需要文件名时,用 path.basename(userInput) 获取纯净的文件名段,丢弃路径部分。

3. 拼接后解析规范路径,并验证前缀

先与安全的基础目录拼接,然后使用 path.resolve() 得到绝对路径,再检查该路径是否仍以基础目录开头。这是最可靠的防御手段。

const baseDir = path.resolve('/var/data');
const userFile = req.query.file;

const fullPath = path.resolve(baseDir, userFile);

if (!fullPath.startsWith(baseDir + path.sep)) {
  throw new Error('非法路径');
}

注意:必须使用 path.sep 确保完整匹配,避免攻击者通过创建 /var/data_evil 绕过简单的 startsWith(baseDir)

4. 拒绝绝对路径和一切不规范字符

可以在接收参数时就进行白名单校验:只允许字母、数字、下划线、短划线、点等安全字符,并长度限制。

if (!/^[a-zA-Z0-9_\-.]+$/.test(userFile)) {
  throw new Error('文件名包含非法字符');
}

5. 使用经过安全审计的中间件

在 Express 中,如果一定要使用 res.sendFile,必须将 root 选项设置为合法的基础目录,并避免直接拼接路径。

// 安全用法:指定 root
res.sendFile(userFile, { root: path.join(__dirname, 'public') }, (err) => {
  if (err) { /* ... */ }
});

send 模块内部会对路径进行安全验证,但若手动拼接后传入,则防护会被绕过,所以必须使用选项的形式传递相对路径。

6. 对未授权文件设置访问控制

即便路径合法,也需要验证当前用户是否有权限访问该文件。例如,URL 为 /files/123 时,服务器应从数据库确认文件 123 属于该用户,而不是让用户随意指定文件名。

7. 压缩包解压时的路径穿越

在解压用户上传的 ZIP 文件时,解压条目中可能包含 ../../ 穿越路径。必须使用安全的解压库(如 adm-zip 不提供自动防止),或解压前检查每个条目路径是否安全。推荐使用 yauzl 结合手动路径校验,或者使用专门防御穿越的库(如 unzipper)。

const extractPath = path.resolve('/var/uploads/extracted');
zipfile.on('entry', (entry) => {
  const resolvedPath = path.resolve(extractPath, entry.fileName);
  if (!resolvedPath.startsWith(extractPath + path.sep)) {
    // 拒绝解压此条目
    return;
  }
  // 安全解压
});

三、综合防御策略

将文件上传和路径遍历的防护组合起来,一套健壮的安全策略应包括:

  • 最小权限原则:上传目录的 OS 权限设为仅应用可读写,移除执行权限。
  • 纵深防御:除应用层校验外,Web 服务器(Nginx)也可限制上传目录中对脚本的执行。
  • 持续监控:记录所有上传和文件访问操作,异常行为触发告警。
  • 定期审计依赖multerformidable 等上传库的版本更新可能修复路径穿越漏洞,保持依赖最新。

Node.js 的灵活使得安全责任完全落在开发者身上。但只要牢记“不信任任何用户输入”这一铁律,并严格执行上述技术措施,文件上传和路径遍历漏洞完全可以被杜绝。在下一小节,我们将探讨接口限流与防重放攻击的具体实现方案。