FixVibe

// docs / baas security / supabase storage

Supabase 存储桶安全清单:22 项

Supabase Storage 是一个围绕 S3 兼容桶的薄包装,加上与数据库相同的行级安全模型。这意味着影响表的相同 RLS 陷阱也影响文件访问 — 以及在 AI 编码工具配置上传时出现的一些存储特有的问题。本清单跨五个部分有 22 项:桶配置、RLS 策略、上传验证、签名 URL 和运营卫生。每项都可在 15 分钟内验证。

下面的每项都是必需的。对于底层 RLS 机制,请参阅 Supabase RLS 扫描器。对于与存储相邻的密钥暴露类别,请参阅 暴露在 JavaScript 中的 Supabase 服务角色密钥

桶配置

从正确的默认值开始。配置错误的桶无论你的 RLS 是否正确都会泄露文件。

  1. 每个桶默认设为私有。在 Supabase 仪表板 → Storage → Buckets 中,除非你有明确理由 (营销资产、无 PII 的公共头像),否则将 Public bucket 开关设为关闭。公共桶对读取操作绕过 RLS — 任何知道桶名的人都可以列出和下载。
  2. 为每个桶设置严格的文件大小限制。仪表板 → Bucket settings → File size limit。50 MB 是用户上传的合理默认值;对视频 / 大文件用例有意提高。没有限制,单次恶意上传可耗尽你的存储配额或月度带宽。
  3. 限制每个桶允许的 MIME 类型。允许的 MIME 类型列表 — 明确的白名单,而非黑名单。仅图像桶使用 image/jpegimage/pngimage/webp。在用户内容桶中绝不允许 text/htmlapplication/javascriptimage/svg+xml — 通过签名 URL 提供时它们会在浏览器中执行。
  4. 每种内容类型使用一个桶,而不是一个共享桶。每桶设置 (大小、MIME 类型、RLS 策略) 是你拥有的粒度。user-avatars 桶、document-uploads 桶和 public-assets 桶比一个混合桶更易锁定。
  5. 如果前端上传,验证 CORS 配置。如果用户直接从浏览器上传到签名 URL,桶 CORS 必须列出你的生产源。* 只对公共桶可接受 — 绝不用于包含用户 PII 的桶。

storage.objects 上的 RLS 策略

Supabase Storage 将文件元数据存储在 storage.objects 表中。该表上的 RLS 控制谁可以读取、上传、更新或删除文件。没有 RLS,桶的公共/私有标志就是你唯一的保护。

  1. 确认 storage.objects 上 RLS 已启用。 SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects'; 必须返回 true。Supabase 在新项目上默认启用它;请验证它没有被禁用。
  2. 为私有桶编写按 auth.uid() 限定范围的 SELECT 策略。 CREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);。约定是将文件存储在 [user-id]/[filename] 下,并使用 storage.foldername() 从路径中提取所有者。
  3. 编写强制相同路径约定的 INSERT 策略。 CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);。没有 WITH CHECK,已认证用户可以上传到另一用户的文件夹。
  4. 如果应用支持文件编辑或删除,添加 UPDATE 和 DELETE 策略。每个命令都需要自己的策略。跳过 DELETE 意味着已认证用户无法删除自己的文件;跳过 UPDATE 意味着文件覆盖会静默失败。
  5. 在两个浏览器会话中测试跨用户访问。以用户 A 登录,上传文件,复制路径。在另一浏览器中以用户 B 登录,尝试通过 REST API 获取文件。响应必须是 403404,绝不是 200
sql
-- Confirm RLS on storage.objects
SELECT rowsecurity
FROM   pg_tables
WHERE  schemaname = 'storage' AND tablename = 'objects';

-- SELECT policy: scope reads to the owning user's folder.
CREATE POLICY "users_read_own_files"
  ON storage.objects
  FOR SELECT
  USING (auth.uid()::text = (storage.foldername(name))[1]);

-- INSERT policy: enforce the [user-id]/[filename] path convention.
CREATE POLICY "users_upload_own"
  ON storage.objects
  FOR INSERT
  WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);

上传验证

即使桶有 MIME 和大小约束,也要在服务端验证每次上传。AI 编码工具默认生成仅客户端验证;那什么都保护不了。

  1. 在服务端从文件的实际字节而非 Content-Type 标头重新检查 MIME 类型。使用像 file-type (Node) 这样的库或魔术字节嗅探。攻击者可以在实际上是多语言 HTML / JavaScript 载荷的文件上声称 Content-Type: image/jpeg
  2. 从上传的图像中剥离 EXIF 元数据。EXIF 可能包含 GPS 坐标、设备序列号和时间戳。在存储前使用 sharp.withMetadata(false)exif-parser 剥离。
  3. 拒绝包含 script 标签或 onload 处理程序的 SVG。SVG 是 XML — 许多 AI 生成的应用允许将 SVG 上传为「只是图像」。在服务端使用 DOMPurify 或完全拒绝 SVG 上传。
  4. 使用确定性的、不可猜测的文件名。不要保留原始文件名。使用 UUID 或文件内容的哈希。原始文件名会泄露信息 ("passport_scan_2024_01_15.jpg"),可预测的名称使枚举成为可能。

签名 URL

签名 URL 是客户端访问私有桶的方式。过期时间、桶范围和被记录的内容都很重要。

  1. 签名 URL 默认过期时间为 1 小时或更短。Supabase JS SDK 的 createSignedUrl(path, expiresIn) 接受秒数。绝不使用像 31536000 (一年) 这样的值 — URL 会变成永久的半公开链接。
  2. 绝不在数据库中存储签名 URL。在每次请求时在服务端生成新的。存储了带 1 年过期的签名 URL,如通过数据库导出泄露,就会授予长期访问。
  3. 记录签名 URL 的生成,而不仅是文件上传。如果之后怀疑有入侵,你需要知道谁、何时生成了哪个 URL。记录 auth.uid() + 桶 + 对象路径 + 时间戳。
  4. 在提供用户上传的文件时使用 downloadAs 选项。 createSignedUrl(path, expiresIn, { download: '.jpg' }) 强制 Content-Disposition: attachment 标头,使文件下载而非渲染 — 击败 HTML / SVG / PDF 中嵌入 HTML 的执行类别。

运营卫生

存储配置会随时间漂移。这四项运营项目能让攻击面保持紧致。

  1. 每季度审计桶。仪表板 → Storage → Buckets。确认公共/私有状态和 MIME 类型列表与应用的预期一致。「临时」创建的桶如果无人删除就会变成永久的。
  2. 监控匿名列出操作。存储日志 (仪表板 → Logs → Storage) 记录 LIST 请求。针对私有桶的匿名列出请求激增意味着有人正从外部探测它。
  3. 为短暂上传设置保留策略。临时桶 (图像预览、草稿上传) 应通过计划函数在 24-72 小时后自动删除。在 GDPR / CCPA 数据最小化义务下,无限期保留是一种责任。
  4. 每月运行 FixVibe 扫描。 baas.supabase-storage-public 检查探测响应匿名 GET + LIST 的桶。新桶会被添加;旧桶会改变可见性 — 只有持续扫描才能捕获漂移。

后续步骤

对你的生产 URL 运行 FixVibe 扫描 — 匿名存储列表出现在 baas.supabase-storage-public 下。将此清单与 Supabase RLS 扫描器 配合用于表层,与 暴露在 JavaScript 中的 Supabase 服务角色密钥 配合用于相邻的密钥暴露。对于其他 BaaS 提供商的存储配置错误,请参阅 BaaS 配置错误扫描器

// 扫描你的 baas 面

在其他人之前找到开放的表。

输入一个生产 URL。FixVibe 会列举你的应用通信的 BaaS 提供商,识别它们的公共端点,并报告未经身份验证的客户端可以读取或写入什么。免费,无需安装,无需信用卡。

  • 免费层 — 每月 3 次扫描,注册无需信用卡。
  • 被动 BaaS 指纹识别 — 无需域名所有权验证。
  • Supabase、Firebase、Clerk、Auth0、Appwrite 等。
  • 每项发现都附带 AI 修复提示 — 可粘贴回 Cursor / Claude Code。
Supabase 存储桶安全清单:22 项 — Docs · FixVibe