Portabase 主密钥被覆盖,1.21.2 已修复(CVE-2026-67449)
Portabase 的 TUS Webhook 存在未认证路径穿越,可覆盖保护加密备份的 AES-256 主密钥;1.21.2 已修复。
TL;DR
- Portabase 的 TUS Webhook 信任
X-File-Path头,并把上传文件重命名到私有目录中的任意路径。该漏洞为 CVE-2026-67449,CVSS v3.1 9.8。 - 自托管的 Community Edition 1.21.1 及更早版本在
/tus/files/或/api/tus/hooks可达时受影响。修复版本为 2026-07-04 发布的 1.21.2。 - 公告包含一个两请求的概念验证,可覆盖
/data/private/keys/master_key.bin。没有已知的实际利用,截至 2026-11-12 该 CVE 不在 CISA KEV 中。 - 升级到 1.21.2 或更高版本;最新版本为 1.36.1。如果无法升级,请在反向代理上阻止
/api/tus/hooks和/tus/。
我受影响吗?
只有以下条件全部成立时才受影响:
- 您运行自托管的 Portabase Community Edition 1.21.1 或更早版本。
- 不可信客户端可以访问该实例,并且
/tus/files/或/api/tus/hooks通过反向代理暴露。 - 攻击者可以创建 TUS 上传并控制
X-File-Path头。不需要账户。
默认 Docker 部署将 nginx 放在 80 端口,把 /tus/ 代理到 tusd 上传服务,并允许 tusd 调用应用上的 /api/tus/hooks。Webhook 也可通过通用的 location / 规则访问,因此只阻止 /tus/ 的代理可能仍让 Webhook 开放。公告指出,TUS 上传端点不要求认证。
| 产品 | 受影响版本 | 修复版本 |
|---|---|---|
| Portabase Community Edition | 1.21.1 及更早 | 1.21.2 |
| Portabase Community Edition | 1.21.2 及更高 | 不受影响 |
Docker Hub 镜像有 398,662 次拉取,GitHub 仓库有 1,899 个星标。受影响范围是 1.21.2 之前的自托管 Community Edition。
检查运行中容器的镜像标签。应用没有版本端点,因此请读取容器创建时使用的镜像:
docker inspect portabase --format '{{.Config.Image}}'
docker compose images | grep -i portabase
如果标签是 latest,请将运行中容器的 digest 与您打算运行的版本的 digest 比较。第一条命令读取运行中的 digest;后两条命令拉取该版本并读取其 digest:
docker inspect portabase --format '{{index .RepoDigests 0}}'
docker pull portabase/portabase:1.36.1
docker image inspect portabase/portabase:1.36.1 --format '{{index .RepoDigests 0}}'
当前版本是 2026-11-10 发布的 1.36.1。1.21.2 及之后的任何标签都包含修复。
如何修复?
升级到 Portabase 1.21.2 或更高版本,并重新创建容器。如果运行中的容器仍使用旧镜像,仅拉取新镜像是不够的。
- 拉取修复后的镜像并重新创建服务:
bash docker compose pull docker compose up -d - 确认运行中的容器使用新镜像:
bash docker inspect portabase --format '{{.Config.Image}}' - 如果固定版本,请使用
1.21.2或更高标签。最新版本是1.36.1。 - 如果从源码构建,请使用提交
8dcb4067或 1.21.2 标签。修复位于 pull request #355。
如果无法立即升级,请在反向代理上阻止上传路径。公告建议对不可信网络拒绝访问 /api/tus/hooks 和 /tus/。这会关闭远程攻击者的路径,但也会停止合法的代理上传,因此请安排升级。
- 在 nginx 中,在通用 location 之前拒绝 Webhook:
nginx location = /api/tus/hooks { deny all; return 404; } - 将
/tus/限制为运行备份代理的网络,或将其从公共监听器中移除。 - 重新加载代理,并确认来自不可信网络的 Webhook 返回 404。
阻止 Webhook 是临时缓解措施,不是修复。通过其他路径到达应用的请求仍可利用路径穿越。
根本原因是什么?
TUS Webhook 从 HTTP 头中获取文件路径,并在不检查 .. 的情况下将其拼接到上传目录。path.join() 会规范化路径穿越,因此结果路径可以离开上传目录并到达主密钥。
漏洞代码位于 1.21.1 标签的 app/api/tus/hooks/route.ts。以下是相关行:
const filePath = headers["X-File-Path"]?.[0];
if (!filePath) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
...
fs.renameSync(oldFilePath, newFilePath);
容器中的 PRIVATE_PATH 默认为 /data/private。上传目录是 /data/private/uploads/,上传的临时文件是 /data/private/uploads/tmp/<id>。头值 ../keys/master_key.bin 使 newFilePath 解析为 /data/private/uploads/../keys/master_key.bin,即 /data/private/keys/master_key.bin。fs.mkdirSync() 会在需要时创建父目录,fs.renameSync() 会原子地替换目标。
主密钥是存储在 keys/master_key.bin 中的 32 字节随机值,权限为 0600。Portabase 用它进行备份数据的 AES-256-GCM 加密。同一文件由 src/utils/rsa-keys.ts 中的 getOrCreateMasterKey() 创建,Webhook 的目标也是同一路径。
头由攻击者控制,因为 tusd 会把请求头转发给 Webhook。上传端点不要求认证,因此未认证客户端可以开始上传并在 X-File-Path 中设置路径穿越。Webhook 信任该头,因为它期望 tusd 发送代理选择的路径。
如何被利用?
- 攻击者创建带有路径穿越的 TUS 上传,
X-File-Path指向目标。上传服务返回新上传的位置。 - 攻击者向该位置上传 32 字节。上传完成后,tusd 用原始头调用 Webhook。
- Webhook 将
../keys/master_key.bin拼接到上传目录,必要时创建keys目录,并把临时上传重命名覆盖主密钥。 - 攻击者现在控制密钥材料。用旧密钥加密的现有备份无法解密。覆盖后加密的数据对知道新密钥的人可读。
公告发布了一个两请求的概念验证。以下是从公告复制的请求序列:
# Step 1: create the upload
curl -X POST http://<host>/tus/files/ \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 32" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success"
# Step 2: upload the attacker-controlled key material
curl -X PATCH http://<host>/tus/files/<id> \
-H "Tus-Resumable: 1.0.0" \
-H "Content-Type: application/offset+octet-stream" \
-H "Upload-Offset: 0" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success" \
-d "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
公告示例使用 32 个 A 字节。真实攻击者会选择自己知道的密钥。公告没有发布受害者报告或实际利用。
攻击如何展开?
未认证的上传请求在头中携带路径穿越,Webhook 在不检查路径指向的情况下把上传文件重命名覆盖 AES 主密钥。
flowchart LR
A["POST /tus/files/<br/>X-File-Path: ../keys/master_key.bin"] --> B["tusd 保存上传<br/>private/uploads/tmp"]
B --> C["tusd 调用<br/>/api/tus/hooks"]
C --> D["path.join(uploadDir, filePath)<br/>不检查路径穿越"]
D --> E["fs.renameSync()<br/>覆盖 master_key.bin"]
E --> F["旧备份无法解密<br/>新密钥被攻击者掌握"]
攻击者能做什么?
未认证攻击者可以覆盖 Portabase 进程在私有目录中可写的任何文件。最危险的目标是 AES-256-GCM 主密钥。覆盖它会使所有用旧密钥加密的备份永久不可读,这是对恢复能力的破坏性拒绝服务。
如果攻击者写入自己知道的密钥,他们还可以读取覆盖后加密的数据。这包括新备份以及 Portabase 用同一密钥加密的任何其他数据。公告描述了两种结果:现有备份的破坏性丢失,以及用攻击者已知密钥加密的数据泄露。
CVE 记录将该漏洞评为 CVSS v3.1 9.8,CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H。
攻击者需要网络可达,不需要账户。路径固定在源码中,因此攻击者不需要发现私有目录结构。唯一前提是 TUS 上传或 Webhook 可达。
为什么修复有效?
补丁增加了三层。第一,将头限制为严格的路径模式。第二,验证解析后的路径仍在上传目录内。第三,将 Webhook 调用绑定到现有备份记录,并在 nginx 边缘阻止 Webhook。
+const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
...
- const fileName = headers["X-File-Name"]?.[0];
const filePath = headers["X-File-Path"]?.[0];
- if (!filePath) {
- return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
+ if (!filePath || !FILE_PATH_RE.test(filePath)) {
+ log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
}
+
+ const generatedId = headers["X-Generated-Id"]?.[0];
+ const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
...
+ const resolvedBase = path.resolve(uploadDir);
+ const newFilePath = path.resolve(resolvedBase, filePath);
+ const rel = path.relative(resolvedBase, newFilePath);
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
+ log.warn({filePath}, "Rejected path traversal in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
+ }
正则表达式只接受 backups/YYYY-MM-DD/<filename>,字符集有限,因此 ../keys/master_key.bin 在任何文件系统调用之前被拒绝。path.resolve() 和 path.relative() 检查随后捕获任何逃出上传目录的路径,即使以后模式被修改。
授权块要求 X-Generated-Id 和 X-Backup-Storage-Id 与数据库和备份存储记录匹配,因此伪造的 Webhook 调用会被拒绝。nginx 规则从公共边缘拒绝 /api/tus/hooks。
补丁覆盖了 Webhook 中的路径穿越。公告还指出,内部 tusd Webhook 现在只能通过 loopback 访问,从而移除了概念验证使用的外部路径。
如何检查是否已遭入侵?
没有公开的入侵指标。覆盖会修改主密钥文件,因此如果有正常副本,请与之比较。
检查文件大小和修改时间:
docker exec <container> ls -l /data/private/keys/master_key.bin
docker exec <container> stat /data/private/keys/master_key.bin
文件应为 32 字节,修改时间应与安装或最后一次主动轮换一致。如果您没有轮换过该文件,但修改时间是最近的,这是一条线索。
将密钥哈希与密钥备份比较:
docker exec <container> sha256sum /data/private/keys/master_key.bin
在私有目录中查找意外文件,尤其是密钥旁边的 .meta 文件:
docker exec <container> find /data/private -maxdepth 3 -type f -newermt "2026-07-04"
备份恢复失败是另一个信号。如果怀疑覆盖之前创建的备份不再能解密,密钥可能已被替换。在重启或重建之前保留容器和私有目录,因为重启可能覆盖证据。
时间线
- 2026-07-04:Portabase 发布公告 GHSA-7697-vf3j-253j 和包含修复的 1.21.2。
- 2026-07-29:预留 CVE ID。
- 2026-11-11:CVE-2026-67449 记录发布,距修复四个月。
常见问题
所有 Portabase 安装都受影响吗?
不是。公告涵盖 1.21.1 及更早的自托管 Community Edition。1.21.2 或更高版本的安装已修复。不可信客户端无法访问的实例不会通过此路径暴露。
TUS 端点应该是公开的吗?
默认部署通过 nginx 暴露 /tus/,并且不认证上传。修复在代理处阻止内部 Webhook,并将 Webhook 调用绑定到备份记录。如果您将 Portabase 暴露到互联网,请检查两条路径。
攻击者能读取我的现有备份吗?
覆盖密钥会使现有备份不可读,但不会解密它们。如果攻击者写入自己知道的密钥,他们可以读取覆盖后加密的数据。公告描述了破坏性和泄露两种结果。
有公开利用吗?
公告包含一个两请求的概念验证。没有公开的实际利用或受害者报告,截至 2026-11-12 该 CVE 不在 CISA KEV 中。
如果我已经运行更高版本呢?
检查运行中容器的镜像标签,而不是 compose 文件中的标签。升级前创建的容器可能继续运行旧镜像,直到被重新创建。1.21.2 及之后的任何标签都包含修复。
阻止 /tus/ 会破坏备份吗?
如果您的备份代理通过该路径上传,会破坏。仅阻止 /api/tus/hooks 会从公共边缘关闭 Webhook,但应用可能仍通过其他路径可达。请尽快升级。
参考文献
- Portabase 公告 GHSA-7697-vf3j-253j,2026-07-04 发布:https://github.com/Portabase/portabase/security/advisories/GHSA-7697-vf3j-253j(2026 年 11 月 12 日检查)
- CVE-2026-67449 记录,GitHub CNA,2026-11-11 发布:https://www.cve.org/CVERecord?id=CVE-2026-67449(2026 年 11 月 12 日检查)
- 修复提交 8dcb4067,pull request #355:https://github.com/Portabase/portabase/commit/8dcb406790728fbdbf758ec88bf000af79498fc1(2026 年 11 月 12 日检查)
- Portabase 1.21.2 发布:https://github.com/Portabase/portabase/releases/tag/1.21.2(2026 年 11 月 12 日检查)
- Portabase 仓库和当前版本 1.36.1:https://github.com/Portabase/portabase(2026 年 11 月 12 日检查)
- Docker Hub,portabase/portabase 拉取次数:https://hub.docker.com/r/portabase/portabase(2026 年 11 月 12 日检查)