12k
All articles

在 Node.js 中防止路径遍历攻击

通过path.resolve和path.sep范围检查防止Node.js中的路径穿越,并用基于ID的文件查找替代用户输入。

OpenReplay Team
OpenReplay Team
在 Node.js 中防止路径遍历攻击

如果你的 Express 应用基于用户输入提供文件服务——比如下载路由、上传预览或动态资源——那么你就存在潜在的路径遍历漏洞。这是 Node.js 中最常见且最具破坏性的文件安全错误之一,而且令人惊讶的是,在不知不觉中就很容易引入这个问题。

本文将解释路径遍历攻击的工作原理,为什么常见的”修复”方法不够充分,以及在实践中 Node.js 的安全文件处理应该是什么样的。

核心要点

  • 路径遍历攻击利用对用户提供的文件路径缺失或不完整的验证,允许攻击者访问预期目录之外的文件。
  • path.normalize()path.join() 不是安全工具——它们只是清理路径,但不能防止目录遍历。
  • 正确的防御方法是使用 path.resolve() 解析路径,并使用包含 path.sepstartsWith 检查来验证结果是否保持在允许的基础目录内。
  • 最安全的方法是完全避免使用用户提供的路径,通过基于 ID 的查找将输入映射到文件。

什么是路径遍历攻击?

路径遍历攻击(也称为目录遍历)发生在攻击者操纵文件路径输入以访问预期目录之外的文件时。经典示例:

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

如果你的服务器天真地将该输入与基础目录连接并读取文件,你就刚刚泄露了系统的密码文件。

这种攻击并不复杂。它利用了缺失或不完整的输入验证——具体来说,是未能验证解析后的路径是否保持在允许的目录内。

为什么 path.normalize()path.join() 不够用

这是理解 Node.js 路径遍历防护最重要的一点:这些工具不是安全工具

path.normalize() 解析 .. 序列并清理冗余斜杠。path.join() 连接路径段。这两者都不能防止遍历——它们只是生成一个更清晰的路径,但该路径仍可能指向预期目录之外。

考虑这个常见但不安全的模式:

// ❌ 不安全:path.join 不能防止遍历
app.get('/download', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.query.file)
  res.sendFile(filePath)
})

如果 req.query.file../../etc/passwd,path.join() 会干净地解析它——然后发送错误的文件。

URL 编码使情况变得更糟。攻击者可能发送 ..%2F..%2Fetc%2Fpasswd。Express 会自动解码 URL 参数,因此你应该验证接收到的解码值。如果你自己处理原始 URL,在验证之前先解码一次(并进行适当的错误处理)。

正确的模式:解析并验证包含关系

Node.js 中可靠的安全文件处理方法是:

  1. 使用 path.resolve() 将用户提供的路径相对于基础目录进行解析。
  2. 验证解析后的路径是否保持在该目录内。
const path = require('path')

const BASE_DIR = path.resolve(__dirname, 'uploads')

function safeResolve(userInput) {
  const resolved = path.resolve(BASE_DIR, userInput)

  // 确保解析后的路径在 BASE_DIR 内
  if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
    return null
  }

  return resolved
}

// ✅ 安全的 Express 文件下载路由
app.get('/download', (req, res) => {
  const safePath = safeResolve(req.query.file)

  if (!safePath) {
    return res.status(403).send('Access denied')
  }

  res.sendFile(safePath)
})

注意 path.sep 的添加。如果没有它,基础目录为 /uploads 时会错误地允许 /uploads-other/secret.txt 通过前缀检查。

在 Windows 上,path.resolve()path.sep 能正确处理反斜杠,因此这个模式可以跨平台工作。为了更严格的验证,你还可以使用 path.relative() 比较路径,并拒绝任何逃离基础目录的结果。

请注意,符号链接仍然可以指向基础目录之外。如果你要提供敏感文件,在发送之前使用 fs.realpath() 解析真实路径。

更好的方法:完全避免使用用户提供的路径

最安全的方法是根本不使用用户提供的路径。相反,通过间接方式将用户输入映射到文件:

// ✅ 基于 ID 的文件查找——不从用户输入构造路径
const FILES = {
  'report-2024': 'reports/annual-2024.pdf',
  'invoice-001': 'invoices/inv-001.pdf',
}

app.get('/download/:id', (req, res) => {
  const filePath = FILES[req.params.id]

  if (!filePath) {
    return res.status(404).send('Not found')
  }

  res.sendFile(path.resolve(__dirname, 'secure', filePath))
})

这完全消除了遍历风险。如果你的使用场景允许——文件下载、文档导出、用户特定资源——这是首选的模式。

关于 Express 静态文件服务的说明

express.static() 在提供公共资源时通常更安全,因为它将访问限制在定义的根目录内。风险出现在你编写自定义文件服务路由时,这些路由从请求参数构造路径或暴露意外的目录。配置和根目录选择仍然很重要。

结论

路径遍历是一个简单的漏洞,有一个简单的修复方法:永远不要信任用户输入作为文件路径。将其相对于固定基础目录解析,使用 path.resolve() 和包含 path.sepstartsWith 检查验证包含关系,并在使用前验证解码后的输入。如果可能,优先使用基于 ID 的查找,完全避免从用户输入构造路径。path.normalize()path.join() 是有用的工具——只是不能保证安全。

常见问题

express.static() 本身能防止路径遍历吗?

它通过从定义的根目录提供文件并在内部处理路径解析来提供帮助,但不能保证完全安全。配置错误或暴露敏感目录仍然可能造成风险。使用用户输入的自定义文件服务路由是大多数漏洞发生的地方。

检查用户输入中的双点序列是可靠的防御吗?

不是。过滤或拒绝包含双点序列的字符串是脆弱的。攻击者可以通过 URL 编码、双重编码或特定平台的路径分隔符绕过此类检查。可靠的防御是使用 path.resolve() 解析完整路径,然后使用 startsWith 检查或 path.relative() 验证结果是否在预期的基础目录内。

这个路径遍历防护模式在 Windows 上有效吗?

是的。使用 path.resolve() 和 path.sep 使包含检查能够跨平台工作。path.resolve() 会规范化 Windows 路径,path.sep 会计算为正确的分隔符。为了更严格的验证,使用 path.relative() 确保解析后的路径不会逃离基础目录。

在发送文件之前我还应该检查文件是否存在吗?

是的。在确认解析后的路径在基础目录内之后,在调用 res.sendFile() 之前使用 fs.existsSync() 或 fs.access() 验证文件是否存在。这可以防止通过错误消息泄露目录结构信息,并让你在文件缺失时控制响应。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.