文章总结: 本文详细分析了基于Supabase的多租户SaaS应用中存在的跨租户写入和SSRF漏洞。攻击者通过解析客户端代码枚举边缘函数,利用服务角色绕过行级安全限制,将恶意文件写入任意租户存储桶。文章提出了四步方法论:客户端API枚举、输入字段验证、服务角色权限检查、单会话越界证明,为渗透测试提供实用指导。 综合评分: 85 文章分类: 渗透测试,WEB安全,代码审计,红队,安全开发
阅读 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', { 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
- 输出路径由攻击者控制。
- 边缘函数运行服务角色——Supabase的密钥,绕过RLS。
所以真正的问题是:它会检查我所在的工作区吗?我设置了另一个工作区的 ID,并发送了它。响应:,文件写入该外部工作区下的路径。submissionId``submissionId``success: true
第五步——证明这是真实的边界越界(不是假阳性)
这一步让“有趣”变成“确认”,你可以在一次会话中完成——让应用自己的授权证明你是外人,然后证明你仍然越界了:
[1] who am I? → user in WORKSPACE_B only
[2] my workspaces → [WORKSPACE_B] // victim ABSENT
[3] my membership in A → [] // not a member
[3b] can I read A? → [] // RLS denies
[4] write toA via fn → success, path: A/poc.zip // it wrote anyway
[5] direct upload to A → 400"row-level security policy" // boundary is real
[2]/[3]/[3b]是RLS的答案——服务器本身显示我与工作区A没有关系。 证明直接写入该处被正确拒绝。但它却把我(SSRF获取的)内容写入A的存储。矛盾在于漏洞——而且不需要第二个版本来证明这一点。[5]``[4]
影响
任何经过认证的用户都可以将任意、受攻击者控制的文件写入/覆盖到任何租户的存储桶中(数据篡改/中毒;可能覆盖合法文件;如果这些文件被返回给该工作区用户,则实现跨租户内容传递)——还有SSRF。
方法论,四行
-
客户端就是你的 API 规范
——从捆绑包中枚举边缘函数/表/RPC。
-
每一个输入都是一个问题
——它是否被取用了?被信任?用我的瞄准镜对照过?
-
遵循服务角色
——管理员/服务角色代码路径是RLS的终结点;这就是租户隔离打破的地方。
-
用一次会话证明
界限——让应用自己的认证证明你是外来者,然后展示你越界了。这正是它不可否认的原因。
感谢阅读!🙏 如果这些有帮助,可以和安全人员分享。下次😘见
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全狗的自我修养 haidragon haidragon《阅读 JavaScript 给了我一个跨租户 Write + SSRF》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论