Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-19264 — CVE-2026-19264 - Postiz(< 2.22.1)中严重的未认证路径遍历漏洞,可导致完全接管实例。技术分析:解码顺序绕过、JWT_SECRET 提权,以及上游修复的分析。 | Kitploit
工具/GitHubGitHub/darklycn1976/cve-2026-19264
漏洞分析代码分析漏洞利用Web应用程序漏洞利用Web安全学习与教育
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 - Postiz(< 2.22.1)中严重的未认证路径遍历漏洞,可导致完全接管实例。技术分析:解码顺序绕过、JWT_SECRET 提权,以及上游修复的分析。

查看仓库
21个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
网站
分享

CVE-2026-19264 - Postiz 中未经验证的路径遍历导致完全接管实例

CVE-2026-19264 - Postiz 中未经验证的路径遍历导致完全接管实例

作者: Krithik Babu P (@DarkLycn1976) 发布时间: 2026-08-10 CVE: CVE-2026-19264 严重性: 严重 - CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 - 对受限目录中路径名的限制不当 受影响版本: gitroomhq/postiz-app < 2.22.1 修复版本: v2.22.1


TL;DR

Postiz 通过一条路由提供本地存储的媒体文件,该路由将 URL 提供的路径段拼接到上传目录上并将结果流式返回——既没有路径规范化,也没有包含性检查,更没有身份验证。

显而易见的路径遍历载荷会返回 404,因为 Next.js 在路由之前就压缩了 ../ 段。但经过 URL 编码的分隔符能够在路由匹配中存活,并且在通往文件系统调用的途中恰好再被解码一次,从而在所有检查的对侧恢复了路径遍历。

未经验证的攻击者可以读取应用程序进程可读的任何文件——包括保存有 JWT 签名密钥的自身环境。由于 Postiz 使用该密钥签署会话令牌,并且签发的令牌不包含过期声明,因此恢复该密钥可将文件读取原语转化为可永久伪造的、以任意用户(包括管理员)身份登录的会话。

一次未经验证的 GET 请求即可完全接管实例。


1. 背景

Postiz 是一个开源社交媒体排程平台——撰写本文时约有 34,000 个 GitHub Star——由 Next.js 前端和 NestJS 后端构建。它被众多代理机构和小型团队广泛自托管,用于管理已连接的社会媒体账户、排程内容和计费。

自托管部署可以将上传的媒体存储在本地,而非对象存储。该行为由单个环境变量控制:

root@kitploit:~
STORAGE_PROVIDER=local

这是 .env.example 中内置的值,因此除非部署者特意配置 S3 或 Cloudflare R2,否则大多数自托管用户都会运行此配置。

2. 攻击面

当本地存储启用时,next.config.js 会将公共路径 /uploads/:path* 重写到内部 API 路由:

root@kitploit:~
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts

在涉及任何漏洞之前,这条路由有两个特性使其值得关注:

  1. 它不需要身份验证。 没有前端中间件保护它。提供公共媒体是设计意图,因此不需要会话。
  2. 它是全捕获(catch-all)路由。 [[...path]] 可选全捕获段意味着所有剩余的路径组件都会以数组形式到达,由处理器自行解释。

当 STORAGE_PROVIDER 不是 local 时,重写指向 /404,处理器不可达。该配置门是部署与这个漏洞之间的唯一屏障。

3. 易受攻击的代码

v2.22.1 之前的处理器:

root@kitploit:~
export const GET = async (request: NextRequest, context) => {
  const { path } = await context.params;
  const filePath =
    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');

  const response  = createReadStream(filePath);
  const fileStats = statSync(filePath);
  // ... stream the file back to the caller
};

四行代码中存在三个缺陷:

  • 没有规范化。 path.normalize()、path.resolve()——都没有调用。传入的段被原样拼接。
  • 没有包含性检查。 没有验证最终得到的 filePath 仍位于 UPLOAD_DIRECTORY 之内。
  • 字符串拼接,而非路径连接。 + '/' + 将组件视为文本,而非具有语义的路径。

结果直接进入 createReadStream(),字节流以根据文件名推断出的 MIME 类型返回给调用者。没有扩展名白名单,也没有内容过滤器。

4. 为什么显而易见的载荷会失败

教科书式的攻击是:

root@kitploit:~
GET /uploads/../../../etc/passwd

在 Postiz 上这会返回 404,而这个 404 正是这个漏洞能存活至今被发现的全部原因。

Next.js 在路由期间会规范化请求路径。原始的 ../ 段在路由器决定调用哪个处理器之前就被压缩了。当请求到达全捕获路由时,路径遍历已被消除——要么路径解析到某个没有匹配路由的位置,要么解析回 /uploads 内部且点号段已消失。

对于快速测试的人来说,这个 404 看起来像是*"框架已经处理了这个问题"*。它确实是一个真实有效的防御。问题不在于它不存在——而在于它在整个处理管线中的执行位置。

5. 绕过——解码顺序不匹配

路由匹配和请求处理器执行的百分号解码次数不同。

如果分隔符经过百分号编码,那么该序列在路由匹配期间就不是路径分隔符。%2e%2e%2f 只是一个不透明字符串——规范化器没有理由去触碰的惰性文本。它完好无损地穿过路由,被全捕获段匹配,并在进入处理器的 params 途中被解码,重新变成 ../。

此时它被拼接到 UPLOAD_DIRECTORY 之后并交给 createReadStream()——已经越过路由、越过规范化、越过所有本可阻止它的控制。

可用的载荷形式:

root@kitploit:~
GET /uploads/%2e%2e%2fsecretdir%2fsecret.txt        → 200, 上传目录之外的文件
GET /uploads/..%2f..%2f..%2fetc%2fpasswd            → 200
GET /uploads/%2e%2e%2f%2e%2e%2f...%2fetc%2fpasswd   → 200, 返回了真实的 /etc/passwd

双重编码无法生效——%252e 在单次解码过程中保持字面量,永远不会变成点号。恰好一层的编码才是最佳平衡点,这也有力地提醒人们:"编码得更狠一点"并不是一种策略。

需要记住的不变式:

在解码完成之前运行的控制,并不能保护最终的数据汇(sink)。

6. 升级——从任意文件读取到实例接管

文件读取原语本身已是高危。它之所以达到严重级别,是因为它能触及的东西。

第 1 步——读取环境。 Node 进程自身的配置就位于部署根目录的磁盘上。.env 能提供以下信息(包括但不限于):

  • JWT_SECRET——会话令牌签名密钥
  • DATABASE_URL——完整的 Postgres 凭据
  • 已连接提供商的 OAuth 密钥和计费密钥

第 2 步——伪造会话。 Postiz 使用 jsonwebtoken 通过 HS256 以 JWT_SECRET 签署会话令牌。关键在于,令牌签发时不设置 expiresIn,因此伪造的令牌永久有效。

第 3 步——成为任何人。 身份验证中间件使用 id 声明从数据库重新解析用户。它特意不信任令牌中诸如 isSuperAdmin 之类的声明——这是良好的设计——但一旦你能签署任意的 id,这种加固就无关紧要了。签署 { id: <受害者用户 id> } 即可产生与合法登录无法区分的会话:

root@kitploit:~
read .env  →  JWT_SECRET  →  sign({ id: victim })  →  authenticated as victim, forever

我使用项目真实的验证逻辑和实际的 jsonwebtoken 依赖对此进行了验证:使用恢复出的密钥签署的令牌被接受,而使用错误密钥签署的同一令牌被拒绝。对照实验至关重要——没有它,你得到的只是假设,而不是发现。

第 4 步——并行路径。 仅凭 DATABASE_URL 就足以直接访问 Postgres:读取每一个已连接的账户,或直接翻转管理员标志。

无需密码。无需先前访问权限。无需用户交互。一次未经验证的 HTTP 请求。

7. 影响与评分

root@kitploit:~
CVSS 4.0  9.3  AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS 3.1  9.8  AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

PR:N 和 UI:N 是起决定性作用的两个度量。该路由不需要会话,也不需要受害者交互——攻击者通过网络独自行动,针对默认配置发起攻击。

唯一诚实的限制因素是配置门:使用 S3 或 R2 的部署不会受到攻击,因为路由被重写到 /404。这缩小了受影响人群的范围,但不会降低其中任何人的严重程度——而且 local 是随附的默认值。

8. 修复

维护者的补丁(7936062)只有八行,值得一读,因为它的正确性正是这类修复常常欠缺的:

root@kitploit:~
+import { resolve, sep } from 'path';
...
-  const filePath =
-    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');
+  const base = resolve(process.env.UPLOAD_DIRECTORY!);
+  const filePath = resolve(base, (path ?? []).join('/'));
+  // Confine reads to UPLOAD_DIRECTORY. resolve() collapses any `..` segments
+  // (including URL-decoded ones), so this blocks every path-traversal variant.
+  if (filePath !== base && !filePath.startsWith(base + sep)) {
+    return new NextResponse('Not found', { status: 404 });
+  }

它有两点做得很对:

  1. 在数据汇处进行规范化。 resolve() 在解码完成后压缩 ..,因此遍历如何绕过路由被走私进来并不重要。检查现在位于危险所在之处。
  2. 它与 base + sep 比较,而非 base。 朴素的 filePath.startsWith(base) 会接受 /app/uploads-evil/x 作为 /app/uploads 内部路径——这是经典的前缀匹配绕过。追加分隔符堵住了它,而 filePath !== base 子句确保目录本身仍然有效。

这就是包含性检查的正确形态:先解析,再与带尾部分隔符的基路径比较。

9. 披露时间线

除非另有说明,时间均为 UTC,2026-07-20。

时间事件
05:55公告报告给 Postiz 团队
07:44维护者确认并验证
12:18修复已提交、验证并发布
2026-08-07 14:13CVE-2026-19264 由 Postiz(CNA)分配

从报告到补丁发布仅用时六小时二十三分,而且这是一个没有漏洞赏金计划的开源项目。我曾见过一些报告在配备专职安全团队的组织中搁置数月无人处理。感谢 Enno Gelhaus 负责协调,感谢 Nevo David 完成修复。

10. 要点总结

一项能通过你测试的控制,并不意味着它处于正确的位置。 那个 404 是真实存在的。Next.js 确实会压缩 ../。只是这道防御在输入完成解码之前就执行了,这意味着它保护的是路由器,而不是文件系统调用。当你发现一处缓解措施时,要问它相对于数据汇何时执行——而不仅仅是它是否存在。

编码是一层,而各层被剥离的速度不同。 只要请求管线中的两个组件对解码次数产生分歧,它们之间的间隙就可被利用。路由匹配器、中间件和处理程序经常存在分歧。

要根据进程能触及到什么来评定文件读取原语,而不是根据原语本身。 "任意文件读取"听起来像是信息泄露。它之所以变成严重级别,是因为环境可读、其中的密钥签署会话、而这些会话永不过期。在评分之前,先顺着链条走一遍。

永不过期的令牌会把泄露变成永久性入侵。 短生命周期令牌的签名密钥泄露已经是很糟糕的一天。在没有 expiresIn 的情况下,不轮换密钥就无法挽回——而大多数运维人员永远不会知道自己需要轮换。

运行对照实验。 验证使用错误密钥签署的令牌会被拒绝,这正是区分"已证实的发现"与"想当然的发现"的关键。

11. 参考资料

  • CVE 记录 - https://www.cve.org/CVERecord?id=CVE-2026-19264
  • GitHub 安全公告 - https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-4hgh-5rhf-4qpm
  • Postiz CNA 公告 (PSA-2026-TH12B7) - https://gadvisory.org/advisories/PSA-2026-TH12B7
  • 修复提交 - https://github.com/gitroomhq/postiz-app/commit/7936062
  • 已修补版本 v2.22.1 - https://github.com/gitroomhq/postiz-app/releases/tag/v2.22.1
  • CWE-22 - https://cwe.mitre.org/data/definitions/22.html

修复指导

如果您自托管 Postiz:

  1. 升级到 v2.22.1 或更高版本。 这就是修复方案。
  2. 如果您在可公开访问的主机上使用 STORAGE_PROVIDER=local 运行过受影响版本,请假定 JWT_SECRET 已泄露。 轮换它。由于令牌没有过期时间,轮换是使任何已伪造令牌失效的唯一方法。
  3. 轮换同一环境中保存的 DATABASE_URL 凭据以及任何已连接提供商的 OAuth 密钥。
  4. 检查访问日志中是否包含 %2e 或 %2f 的 /uploads/ 的 GET 请求。

研究独立进行,并依据协调披露流程向供应商披露。在修复发布且公告公开后发表。未访问任何第三方系统——所有验证均针对从项目自身源码构建的本地实例进行。


许可证

本文遵循 CC BY 4.0 许可——可自由共享和改编,但需署名。来自 gitroomhq/postiz-app 的代码摘录仅用于安全分析引用,仍受该项目许可证约束。

Krithik Babu P - @DarkLycn1976

下载工具
2026-08-07 14:15GitHub 安全公告发布