Node-tar Hardlink 路径穿越漏洞(CVE-2026-24842)深度教学
一、 漏洞概述
CVE编号: CVE-2026-24842
影响组件: Node-tar(Node.js下的TAR文件处理库)
影响版本: tar < 7.5.7
修复版本: tar@7.5.7
漏洞类型: Hardlink Path Traversal (硬链接路径穿越)
核心危害: 通过构造恶意TAR文件中的硬链接(hardlink)条目,使得在解压目录内创建的文件名,实际指向解压目录外的已存在文件,从而导致任意文件读取和任意文件覆盖。
二、 漏洞本质与攻击面演变
2.1 从传统路径穿越到对象绑定绕过的范式转变
传统归档提取漏洞(如Zip Slip)的攻击模式是路径字符串穿越。攻击者通过在压缩包中构造包含../的路径字符串(例如../../../etc/passwd),欺骗解压程序将文件直接写出到目标目录之外。其防御重点在于校验解压后的路径字符串是否仍在目标目录内。
而CVE-2026-24842代表的是一种新的攻击范式:文件系统对象绑定绕过。攻击者利用归档中的特殊条目(如符号链接symlink、硬链接hardlink),使得最终被读取或写入的文件系统对象脱离预期边界,即使其路径字符串看起来完全位于目标目录内。
关键差异对比表:
| 攻击类型 | 攻击原语 | 核心问题 | 主要影响 |
|---|---|---|---|
| 传统路径穿越 | ../, 绝对路径 |
目标路径规范化不严,字符串校验绕过 | 直接越界写文件 |
| 符号链接绕过 | symlink |
提取时跟随符号链接,真实落点不在边界内 | 越界写文件/覆盖文件 |
| 硬链接绕过 | hardlink |
解压目录内对象与边界外已有对象指向同一inode | 越界读写已有文件 |
| 链式链接 | link 指向 link |
校验与落地阶段使用的真实路径不一致 | 绕过单点校验 |
2.2 硬链接(Hardlink)的核心特性
理解本漏洞的前提是理解硬链接的工作机制:
- 硬链接不是指向另一个路径的“快捷方式”(那是符号链接)。
- 硬链接是文件系统层面的概念,它让多个不同的文件名指向同一个底层文件对象(inode)。
- 对其中任何一个文件名的读写操作,实际上都是在操作同一个文件。删除其中一个文件名,只要该inode的链接数(link count)不为0,文件数据就不会被删除。
三、 漏洞原理深度剖析
3.1 攻击流程总览
攻击者构造一个包含硬链接条目的恶意TAR文件,其核心结构如下:
- 一个目录条目:
d/ - 一个硬链接条目:
entry.path(目标文件名):d/xentry.linkpath(链接的源路径):../secret.txt
当业务使用有漏洞的tar.extract()解压此归档到uploads/目录时,预期行为是创建uploads/d/x文件。但由于漏洞存在,实际行为是:在uploads/目录内创建了一个名为d/x的硬链接,该硬链接指向了解压目录外的文件secret.txt。
此后,当业务代码读取uploads/d/x时,实际读取的是secret.txt的内容;写入uploads/d/x时,实际修改的是secret.txt文件本身。
3.2 漏洞根因:校验与落地的语义分离
漏洞的根本原因在于Node-tar在处理硬链接条目时,校验阶段和最终创建阶段所关注的路径对象不一致,导致安全边界被绕过。
以下是Node-tar(漏洞版本)处理硬链接条目的简化调用链及逻辑分析:
-
[ONENTRY]->[CHECKPATH](路径检查):- 检查对象: 主要针对
entry.path(即d/x)进行校验。 - 检查逻辑: 检查
entry.path是否包含..或是否为绝对路径。 - 结果: 由于
d/x是合法相对路径,检查通过。系统计算出entry.absolute为/path/to/uploads/d/x,认为该路径仍在解压目录uploads/内。
- 检查对象: 主要针对
-
[CHECKFS]/[CHECKFS2](文件系统检查):- 虽然
entry.linkpath(即../secret.txt)被加入路径预约数组,但此阶段的主要目的是并发控制和路径冲突避免,并未对entry.linkpath执行与entry.path同等的目录边界安全校验。 - 核心操作仍围绕
entry.absolute(uploads/d/x)展开,如检查父目录存在性等。
- 虽然
-
[MAKEFS](文件创建分发):- 根据
entry.type = Link,将处理逻辑分发到硬链接创建分支([HARDLINK]),不再按普通文件处理。
- 根据
-
[HARDLINK](解析硬链接源路径) - 关键漏洞点:// 简化逻辑 const linkpath = path.resolve(this.cwd, String(entry.linkpath));- 此时代码首次将
entry.linkpath解析为绝对路径。解析基于当前工作目录(this.cwd),即uploads/。 - 代入攻击输入:
path.resolve(‘uploads/’, ‘../secret.txt’)得到的是解压目录外的secret.txt的绝对路径。 - 核心问题: 此前没有任何步骤对这个解析后的路径是否越界进行校验。
- 此时代码首次将
-
[LINK](创建硬链接):- 执行系统调用:
fs.link(linkpath, entry.absolute)。 - 最终效果:
fs.link(‘/absolute/path/secret.txt’, ‘/absolute/path/uploads/d/x’)。 - 结果:
uploads/d/x和../secret.txt成为指向同一个inode的硬链接。
- 执行系统调用:
漏洞根因小结:
校验阶段([CHECKPATH])只确保要创建的文件名(entry.path)是安全的,但没有对该文件名将绑定到哪个文件对象(entry.linkpath)进行同等强度的边界检查。在硬链接创建阶段([HARDLINK]),entry.linkpath被基于工作目录解析,可能指向目录外,而此解析结果未经验证即被用于创建链接,导致安全边界失效。
四、 漏洞复现与实践
4.1 实验环境搭建
- 操作系统: macOS / Linux
- Node.js: v23.11.0
- npm: 10.9.2
- 漏洞版本:
tar@7.5.0 - 修复版本:
tar@7.5.7
目录结构初始化:
.
├── secret.txt # 模拟解压目录外的敏感文件
├── uploads/ # 业务指定的安全解压目录
├── server.js # 模拟业务的服务端代码
└── malicious.tar # 攻击者构造的恶意TAR文件
4.2 构造恶意TAR文件
恶意TAR需包含一个硬链接条目,使其linkpath指向目录外的文件。
可使用以下Node.js脚本构造:
// create-malicious-tar.js
const tar = require('tar');
const fs = require('fs');
const path = require('path');
const cwd = process.cwd();
const tarPath = path.join(cwd, 'malicious.tar');
const pack = tar.c({
gzip: false,
cwd: cwd,
}, ['d/x']); // 要打包的条目,实际通过Stream注入元数据
const ws = fs.createWriteStream(tarPath);
// 手动构建TAR头部,创建指向../secret.txt的硬链接
// ... (具体实现涉及TAR格式字节操作,此处略去)
// 关键:设置 entry.type = 'Link' (硬链接类型), entry.path = 'd/x', entry.linkpath = '../secret.txt'
pack.pipe(ws);
4.3 模拟易受攻击的业务服务
一个简易的HTTP服务,模拟文件上传、解压及后续处理场景:
// server.js
const express = require('express');
const multer = require('multer');
const tar = require('tar'); // 版本 7.5.0
const fs = require('fs');
const path = require('path');
const app = express();
const UPLOAD_DIR = path.join(__dirname, 'uploads');
// 1. 上传并解压接口
app.post('/upload', multer().single('tarfile'), async (req, res) => {
const tarPath = path.join(UPLOAD_DIR, 'upload.tar');
fs.writeFileSync(tarPath, req.file.buffer);
try {
await tar.x({ // 使用有漏洞的tar.extract
file: tarPath,
cwd: UPLOAD_DIR, // 指定解压目录
});
res.send('Upload and extract OK');
} catch (e) {
res.status(500).send('Extract failed');
}
});
// 2. 读取解压文件接口(模拟业务读取)
app.get('/read', (req, res) => {
const filePath = path.join(UPLOAD_DIR, 'd', 'x'); // 业务认为的安全路径
try {
const data = fs.readFileSync(filePath, 'utf8');
res.send(data);
} catch (e) {
res.status(500).send('Read failed');
}
});
// 3. 写入解压文件接口(模拟业务处理写入,如生成缓存、追加元数据)
app.post('/write', express.text(), (req, res) => {
const filePath = path.join(UPLOAD_DIR, 'd', 'x');
try {
fs.appendFileSync(filePath, `\n${req.body}`);
res.send('Write OK');
} catch (e) {
res.status(500).send('Write failed');
}
});
app.listen(3000);
服务逻辑说明:
/upload: 接收TAR文件,使用tar.extract()解压到uploads/目录。业务预期所有文件均被限制在该目录内。/read: 模拟业务读取解压后的文件uploads/d/x。/write: 模拟业务对解压后的文件进行写入操作(如追加内容)。
4.4 漏洞验证步骤
- 启动服务:
node server.js - 上传恶意TAR:
curl -F ‘tarfile=@malicious.tar’ http://localhost:3000/upload - 验证越界读取:
预期结果(漏洞版本):返回curl http://localhost:3000/readsecret.txt的内容,尽管业务代码读取的是uploads/d/x。 - 验证越界覆盖:
预期结果(漏洞版本):curl -X POST -d “HACKED” http://localhost:3000/writesecret.txt文件末尾被添加了“HACKED”字符串。 - 验证inode绑定:
预期结果:两个文件显示相同的inode编号和link count为2,证实它们是同一个文件的硬链接。ls -li uploads/d/x secret.txt
4.5 修复版本验证
- 升级到修复版本:
npm install tar@7.5.7 - 重复上述攻击步骤。
- 预期结果:解压失败或无法创建
uploads/d/x文件,/read接口返回“No such file or directory”,secret.txt内容未被读取或覆盖。
五、 修复方案分析
在修复版本tar@7.5.7中,漏洞在[CHECKPATH]阶段被阻断。
修复核心:在路径检查阶段,不仅检查entry.path,也对硬链接条目的entry.linkpath进行严格的边界校验。
关键代码逻辑(简化):
- 在
[CHECKPATH]中,对于类型为Link的条目,会额外调用[STRIPABSOLUTEPATH](entry, 'linkpath')对entry.linkpath进行校验。 [STRIPABSOLUTEPATH]函数会检查目标字段(此处是linkpath)是否包含..。如果包含,则直接抛出TAR_ENTRY_ERROR并中止该条目的处理。
效果:攻击者提供的entry.linkpath = ‘../secret.txt’在解压流程的早期就被识别并拒绝,硬链接根本不会被创建,攻击链被彻底阻断。
六、 防御建议与最佳实践
-
升级与补丁:
- 立即升级
node-tar到7.5.7或更高版本。
- 立即升级
-
业务层防御:
- 默认拒绝链接:如果业务场景不需要符号链接或硬链接,应在解压前过滤或拒绝包含此类条目的归档文件。
- 独立校验linkpath:对于允许链接的业务,必须对
entry.linkpath进行独立的、与entry.path同等级别的路径边界校验,确保其解析后的绝对路径仍在目标目录内。 - 解压目录隔离:将解压目录设置为独立的、无敏感文件的临时空间或沙箱环境。
- 最小权限原则:运行解压过程的进程应使用最低必要的文件系统权限,以限制越界访问可能造成的损害。
-
安全设计原则:
- 从字符串校验转向对象边界校验:安全设计的重点应从“路径字符串是否包含
..”转变为“最终被操作的文件系统对象是否在可信边界内”。 - 语义一致性:确保在归档处理的整个链路中,对路径的解析、校验和最终使用的语义保持一致,避免出现校验一个路径、使用另一个路径的情况。
- 从字符串校验转向对象边界校验:安全设计的重点应从“路径字符串是否包含
七、 同类漏洞案例(2026年)
该漏洞模式并非孤例,2026年出现的多个归档提取漏洞均与链接处理相关:
| 项目 | 时间 | CVE编号 | 机制 | 影响概括 |
|---|---|---|---|---|
| Node-tar | 2026-01-28 | CVE-2026-24842 | hardlink 目标路径校验不足 | 创建指向外部文件的 hardlink,造成文件读写/覆盖 |
| compressing | 2026-02-04 | CVE-2026-24884 | symlink 目标未充分校验 | 后续文件经 symlink 写入任意位置 |
| Zarf | 2026-03-06 | CVE-2026-29064 | symlink 指向目标目录外 | 造成任意文件读写 |
| Jenkins | 2026-03-18 | CVE-2026-33001 | .tar/.tar.gz 提取时未安全处理 symlink | 可向任意位置写文件 |
共同模式:路径边界 != 对象边界。业务逻辑认为在安全目录内操作,但文件系统通过链接解析,将实际操作对象绑定到了目录之外。
八、 总结
CVE-2026-24842揭示了归档提取类漏洞的一个演进方向:攻击面从简单的路径字符串规范化问题,转向更底层的文件系统对象绑定问题。防御思路需要同步升级,从检查“路径是否安全”,进阶到确保“最终操作的对象是否安全”。对于处理用户上传归档文件的业务,必须对符号链接和硬链接等特殊文件类型保持警惕,并在校验逻辑中给予同等重视。