阅读JavaScript给了我一个跨租户Write+SSRF

admin 2026-06-20 04:43:33 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了基于Supabase的多租户SaaS应用中存在的跨租户写入和SSRF漏洞。攻击者通过解析客户端代码枚举边缘函数,利用服务角色绕过行级安全限制,将恶意文件写入任意租户存储桶。文章提出了四步方法论:客户端API枚举、输入字段验证、服务角色权限检查、单会话越界证明,为渗透测试提供实用指导。 综合评分: 85 文章分类: 渗透测试,WEB安全,代码审计,红队,安全开发


cover_image

阅读 JavaScript 给了我一个跨租户 Write + SSRF

haidragon haidragon

安全狗的自我修养

2026年6月17日 11:55 美国

在小说阅读器读本章

去阅读

作者介绍:http://gitee.com/haidragon

大家好,欢迎回来!👋

请按回车或点击查看全尺寸图片

总结:总结

一个多租户、Supabase支持的SaaS有一个“压缩我的提交文件”的边缘函数,(1)获取我提交的任意URL(SSRF),(2)将归档写入任何工作区的存储文件夹——因为它信任客户端提供的路径,并用服务角色写入,绕过阻挡正常上传的行级安全(RLS)。有趣的不是bug,而是我走的路。这正是这篇文章的主题。

标识符(项目、工作区 ID、程序名称)被涂黑。协调披露后发布。

一段话中的目标

一个基于 Supabase(PostgREST + Storage + Edge Functions)构建的工作区/CRM仪表盘,采用可爱风格的 React 前端。租赁由RLS强制执行:浏览器直接用公共匿名密钥和登录用户的JWT与其通信,数据库决定每个租户能看到什么。架构就是全部故事——所以方法论就是围绕它构建的。https://<project>.supabase.co

步骤1 — 让客户绘制地图

在 Supabase/Firebase/Lovable 应用中,前端就是 API 文档。每个后端调用都存储在 JS 捆包中。所以我没有盲目模糊地操作,而是拉出了捆绑包,重新调用客户端调用的内容:supabase-js

  • .from("…")→表
  • .rpc("…")→数据库功能
  • functions.invoke(\..)→ 边缘功能

函数名被压缩成反向行量字符串;一个 grep 用于 admin-operationsbackup-exportbackup-import-create-portal-sessionzip-submission-files.invoke(\..)dumped the entire Edge Function inventory — including``,``/``,``and the one that mattered:

注意:在BaaS应用中,应从客户端枚举后端,而不是凭猜测端点。

第二步——攻击前阅读合同

该捆绑包甚至附带了应用内的API文档以及完全相同的调用调用:

functions.invoke('zip-submission-files', {&nbsp;body: {
files: [{ url, name }], zipName, submissionId
}})

两个领域立刻引人注目:

  • files[].url—— 服务器将获取的URL列表。
  • submissionId—— 输出写入的路径分量。

注意:客户端提供的每个字段当作一个问题——服务器对此做什么,会检查吗?

第三步——显而易见的问题:它获取了哪个URL?

如果服务器抓取来构建压缩包,我能让它抓我的压缩包吗?我发送了,下载了退回的(签名)压缩包,然后解压——里面包含了示例域名的HTML。files[].url``https://example.com/

SSRF 确认:服务器获取任意 URL,并在归档中给我响应。(云元数据 / / 内部主机名无法从锁定的Deno运行时访问,因此这是外部的全响应SSRF。)localhost

第四步——更好的问题是:它写在哪里?

成功回复将归档写入,直接来自我的请求正文。两个事实发生冲突:task-submissions/<submissionId>/<zipName>``submissionId

  1. 输出路径由攻击者控制
  2. 边缘函数运行服务角色——Supabase的密钥,绕过RLS。

所以真正的问题是:它会检查我所在的工作区吗?我设置了另一个工作区的 ID,并发送了它。响应:,文件写入该外部工作区下的路径。submissionId``submissionId``success: true

第五步——证明这是真实的边界越界(不是假阳性)

这一步让“有趣”变成“确认”,你可以在一次会话中完成——让应用自己的授权证明你是外人,然后证明你仍然越界了:

[1]&nbsp;who am&nbsp;I? &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;→ user in WORKSPACE_B only
[2]&nbsp;my workspaces &nbsp; &nbsp; &nbsp; &nbsp;→&nbsp;[WORKSPACE_B]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // victim ABSENT
[3]&nbsp;my membership in&nbsp;A&nbsp; &nbsp;→&nbsp;[]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// not&nbsp;a&nbsp;member
[3b]&nbsp;can&nbsp;I&nbsp;read&nbsp;A? &nbsp; &nbsp; &nbsp; →&nbsp;[]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// RLS denies
[4]&nbsp;write&nbsp;toA&nbsp;via fn &nbsp; &nbsp;→ success, path: A/poc.zip // it wrote anyway
[5] direct upload to A &nbsp; →&nbsp;400"row-level security policy"&nbsp; // boundary is real

[2]/[3]/[3b]是RLS的答案——服务器本身显示我与工作区A没有关系。 证明直接写入该处被正确拒绝。但它却把我(SSRF获取的)内容写入A的存储。矛盾在于漏洞——而且不需要第二个版本来证明这一点。[5]``[4]

影响

任何经过认证的用户都可以将任意、受攻击者控制的文件写入/覆盖到任何租户的存储桶中(数据篡改/中毒;可能覆盖合法文件;如果这些文件被返回给该工作区用户,则实现跨租户内容传递)——还有SSRF。

方法论,四行

  1. 客户端就是你的 API 规范

    ——从捆绑包中枚举边缘函数/表/RPC。

  2. 每一个输入都是一个问题

    ——它是否被取用了?被信任?用我的瞄准镜对照过?

  3. 遵循服务角色

    ——管理员/服务角色代码路径是RLS的终结点;这就是租户隔离打破的地方。

  4. 用一次会话证明

    界限——让应用自己的认证证明你是外来者,然后展示你越界了。这正是它不可否认的原因。

感谢阅读!🙏 如果这些有帮助,可以和安全人员分享。下次😘见


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:安全狗的自我修养 haidragon haidragon《阅读 JavaScript 给了我一个跨租户 Write + SSRF》

评论:0   参与:  0