【漏洞复现】Node.js 目录穿越漏洞(CVE-2017-14849)
'# 【漏洞复现】Node.js 目录穿越漏洞(CVE-2017-14849)
一、背景与问题
CVE-2017-14849 是 Node.js 在 4.x 版本中暴露的一个严重安全漏洞,属于典型的路径遍历漏洞(Path Traversal Vulnerability)。该漏洞的核心问题在于 Node.js 的 path 模块未正确处理用户输入的文件路径,导致攻击者可以通过构造特殊路径字符串(如 ../../etc/passwd)访问系统文件。
问题场景
在开发文件上传、配置读取或日志处理功能时,若未对用户输入的路径进行严格校验,攻击者可以通过构造恶意路径字符串,绕过预期的访问限制,读取或修改系统文件。例如:
const fs = require('fs');
const path = require('path');
// 恶意输入
const userInput = '../etc/passwd';
const filePath = path.resolve(__dirname, userInput);
console.log(filePath); // 输出:/etc/passwd
fs.readFile(filePath, 'utf8', (err, data) => {
console.log(data);
});上述代码会直接读取系统文件 /etc/passwd,造成严重安全风险。
二、基本原理
1. 路径解析机制
Node.js 的 path.resolve() 函数会将传入的路径字符串转换为绝对路径。其处理逻辑如下:
- 从当前工作目录(
process.cwd())开始,依次处理路径分段。 - 遇到
..时,将路径回退到上级目录。 - 若路径以
/开头,则直接以根目录为起点。
例如:
path.resolve('a', 'b', '..', 'c')
// 会解析为当前工作目录下的 `c`,而不是 `a/b/c`2. 漏洞成因
漏洞的核心在于:path.resolve() 会将用户输入的路径字符串与当前工作目录拼接,而未对路径进行校验。攻击者可以利用 .. 逐层向上访问,最终突破文件系统的边界。
三、环境准备
1. 环境要求
- Node.js 4.x(漏洞存在的版本)
- Node.js 16.x(修复后的版本)
- 代码运行环境:支持
path模块的 Node.js 环境
2. 依赖安装
无需额外安装依赖,只需 Node.js 环境即可。
四、核心实现
1. 漏洞复现代码
// 漏洞复现代码(Node.js 4.x)
const fs = require('fs');
const path = require('path');
// 恶意输入
const userInput = '../etc/passwd';
const filePath = path.resolve(__dirname, userInput);
console.log(`文件路径:${filePath}`);
// 尝试读取文件
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件失败:', err.message);
} else {
console.log('文件内容:', data);
}
});关键代码解释:
path.resolve(__dirname, userInput):将__dirname(当前模块目录)与用户输入拼接,最终得到绝对路径。- 如果
userInput为../etc/passwd,则最终路径为/etc/passwd。
2. 修复方案
在 Node.js 16.x 中,该漏洞已修复。修复方式是增加了对路径的校验逻辑,确保最终路径位于预期的目录范围内。
修复后的代码(Node.js 16.x):
// 修复后的代码(Node.js 16.x)
const fs = require('fs');
const path = require('path');
// 用户输入校验
const userInput = '../etc/passwd';
const baseDir = path.resolve(__dirname, 'safe_dir'); // 预设安全目录
const filePath = path.resolve(baseDir, userInput);
// 校验路径是否在安全范围内
if (filePath.startsWith(baseDir)) {
console.log(`文件路径:${filePath}`);
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件失败:', err.message);
} else {
console.log('文件内容:', data);
}
});
} else {
console.error('路径越界,拒绝访问');
}关键代码解释:
baseDir:预设的安全目录,所有路径必须在此目录下。filePath.startsWith(baseDir):确保最终路径不越界,防止目录穿越。
3. 安全校验方法
- 白名单校验:只允许访问特定目录下的文件。
正则表达式校验:过滤特殊字符(如
..、/)。const allowedChars = /^[a-zA-Z0-9_\-\.\/]+$/; if (!allowedChars.test(userInput)) { throw new Error('非法路径字符'); }
五、完整案例
1. 完整案例:文件上传服务漏洞复现
漏洞代码(Node.js 4.x)
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.post('/upload', (req, res) => {
const userInput = req.query.path; // 用户输入路径
const filePath = path.resolve(__dirname, userInput);
// 漏洞:未校验路径
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
res.status(500).send('读取文件失败');
} else {
res.send(data);
}
});
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});攻击方式:
访问 http://localhost:3000/upload?path=../etc/passwd,将读取 /etc/passwd 文件内容。
修复代码(Node.js 16.x)
app.post('/upload', (req, res) => {
const userInput = req.query.path;
const baseDir = path.resolve(__dirname, 'uploads'); // 预设安全目录
const filePath = path.resolve(baseDir, userInput);
// 校验路径是否在安全范围内
if (filePath.startsWith(baseDir)) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
res.status(500).send('读取文件失败');
} else {
res.send(data);
}
});
} else {
res.status(403).send('路径越界,拒绝访问');
}
});六、源码解析
1. Node.js 4.x 的 path.resolve 实现
在 Node.js 4.x 中,path.resolve 的核心逻辑如下:
// 源码片段(简化版)
void path_resolve(char *path, char *resolved) {
char *cwd = getcwd();
char *current = path;
char *result = resolved;
while (current) {
if (*current == '.' && *(current + 1) == '/') {
current += 2;
} else if (*current == '.' && *(current + 1) == '.') {
// 处理 ..,回退目录
current += 3;
// 逻辑略...
} else {
// 其他处理...
}
}
}2. Node.js 16.x 的修复逻辑
在修复后的版本中,增加了对路径的校验:
// 源码片段(简化版)
void path_resolve_with_check(char *path, char *resolved, char *base) {
char *current = path;
char *result = resolved;
while (current) {
// 原逻辑...
// 新增校验逻辑
if (strncmp(result, base, strlen(base)) != 0) {
// 路径越界,拒绝访问
return;
}
}
}七、进阶使用
1. 使用 path 模块的正确方式
- 使用
path.resolve()时,始终指定一个安全的基目录。 - 避免直接使用用户输入的路径字符串,而是通过
path.join()构建路径。
const safeDir = path.resolve(__dirname, 'safe_dir');
const filePath = path.join(safeDir, userInput);2. 静态文件服务的安全实现
在 Express 中,使用 express.static 提供静态文件服务时,应限制访问目录:
app.use('/static', express.static(path.resolve(__dirname, 'public')));八、性能与工程实践
1. 性能优化
- 路径校验的缓存:对频繁访问的路径进行缓存,避免重复校验。
- 预计算安全目录:在应用启动时预计算安全目录,避免每次请求都重新计算。
2. 异常处理
- 在
fs.readFile中添加try/catch块,避免未处理的异常导致服务崩溃。 - 对用户输入进行日志记录,便于后续安全审计。
3. 安全风险分析
- 权限提升:攻击者可读取系统文件,甚至通过
eval()执行任意代码。 - 数据泄露:敏感数据如数据库配置、用户信息可能被暴露。
九、常见问题与踩坑
1. 常见错误
错误1:未校验用户输入路径,直接拼接使用。
const filePath = path.resolve(userInput); // 错误!解决:始终使用预设的基目录。
错误2:误用
path.join而非path.resolve。const filePath = path.join('a', '..', 'b'); // 输出 'a/../b'解决:使用
path.resolve确保路径正确。
2. 防御误区
- 误区:认为
..会被自动过滤。
原因:path.resolve会将..与当前工作目录结合,导致越界。 - 误区:依赖
path.normalize替代path.resolve。
原因:path.normalize仅处理路径格式,不会处理..的相对路径。
十、最佳实践
1. 安全路径处理规范
- 始终使用基目录:所有路径必须基于预设的目录。
- 严格校验输入:使用正则表达式或白名单校验用户输入。
- 最小权限原则:仅允许访问必需的文件。
2. 开发建议
- 版本控制:确保 Node.js 版本不低于 16.x,避免已知漏洞。
- 安全审计:定期检查代码中所有路径处理逻辑,避免潜在漏洞。
3. 生产环境建议
- 启用安全中间件:如
helmet限制 HTTP 头信息。 - 日志记录:记录所有文件访问请求,便于安全审计。
十一、总结
CVE-2017-14849 是一个典型的 Node.js 路径遍历漏洞,其核心问题在于 path.resolve() 未对用户输入进行校验。通过分析其原理和修复方法,我们可以深入理解路径处理的安全性要求。
在实际开发中,务必遵循以下原则:
- 始终使用预设的基目录,避免直接拼接用户输入。
- 严格校验路径,防止目录穿越和越界访问。
- 升级 Node.js 版本,确保使用已修复的版本。
通过本文的分析,希望开发者能够提高对路径处理安全性的重视,避免类似漏洞的发生。
评论已关闭