// docs / baas security / supabase storage
Supabase 存储桶安全清单:22 项
Supabase Storage 是一个围绕 S3 兼容桶的薄包装,加上与数据库相同的行级安全模型。这意味着影响表的相同 RLS 陷阱也影响文件访问 — 以及在 AI 编码工具配置上传时出现的一些存储特有的问题。本清单跨五个部分有 22 项:桶配置、RLS 策略、上传验证、签名 URL 和运营卫生。每项都可在 15 分钟内验证。
下面的每项都是必需的。对于底层 RLS 机制,请参阅 Supabase RLS 扫描器。对于与存储相邻的密钥暴露类别,请参阅 暴露在 JavaScript 中的 Supabase 服务角色密钥。
桶配置
从正确的默认值开始。配置错误的桶无论你的 RLS 是否正确都会泄露文件。
- 每个桶默认设为私有。在 Supabase 仪表板 → Storage → Buckets 中,除非你有明确理由 (营销资产、无 PII 的公共头像),否则将 Public bucket 开关设为关闭。公共桶对读取操作绕过 RLS — 任何知道桶名的人都可以列出和下载。
- 为每个桶设置严格的文件大小限制。仪表板 → Bucket settings → File size limit。50 MB 是用户上传的合理默认值;对视频 / 大文件用例有意提高。没有限制,单次恶意上传可耗尽你的存储配额或月度带宽。
- 限制每个桶允许的 MIME 类型。允许的 MIME 类型列表 — 明确的白名单,而非黑名单。仅图像桶使用
image/jpeg、image/png、image/webp。在用户内容桶中绝不允许text/html、application/javascript或image/svg+xml— 通过签名 URL 提供时它们会在浏览器中执行。 - 每种内容类型使用一个桶,而不是一个共享桶。每桶设置 (大小、MIME 类型、RLS 策略) 是你拥有的粒度。
user-avatars桶、document-uploads桶和public-assets桶比一个混合桶更易锁定。 - 如果前端上传,验证 CORS 配置。如果用户直接从浏览器上传到签名 URL,桶 CORS 必须列出你的生产源。
*只对公共桶可接受 — 绝不用于包含用户 PII 的桶。
storage.objects 上的 RLS 策略
Supabase Storage 将文件元数据存储在 storage.objects 表中。该表上的 RLS 控制谁可以读取、上传、更新或删除文件。没有 RLS,桶的公共/私有标志就是你唯一的保护。
- 确认 storage.objects 上 RLS 已启用。
SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects';必须返回true。Supabase 在新项目上默认启用它;请验证它没有被禁用。 - 为私有桶编写按
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()从路径中提取所有者。 - Scope permitted INSERT operations to the user’s folder.
CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. With RLS enabled, no applicable INSERT policy means denial; an overly broad applicable policy can permit uploads outside the intended folder. Review all applicable policies and the bucket constraint. - 如果应用支持文件编辑或删除,添加 UPDATE 和 DELETE 策略。每个命令都需要自己的策略。跳过 DELETE 意味着已认证用户无法删除自己的文件;跳过 UPDATE 意味着文件覆盖会静默失败。
- 在两个浏览器会话中测试跨用户访问。以用户 A 登录,上传文件,复制路径。在另一浏览器中以用户 B 登录,尝试通过 REST API 获取文件。响应必须是
403或404,绝不是200。
-- 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 编码工具默认生成仅客户端验证;那什么都保护不了。
- 在服务端从文件的实际字节而非
Content-Type标头重新检查 MIME 类型。使用像file-type(Node) 这样的库或魔术字节嗅探。攻击者可以在实际上是多语言 HTML / JavaScript 载荷的文件上声称Content-Type: image/jpeg。 - 从上传的图像中剥离 EXIF 元数据。EXIF 可能包含 GPS 坐标、设备序列号和时间戳。在存储前使用
sharp的.withMetadata(false)或exif-parser剥离。 - 拒绝包含
script标签或onload处理程序的 SVG。SVG 是 XML — 许多 AI 生成的应用允许将 SVG 上传为「只是图像」。在服务端使用DOMPurify或完全拒绝 SVG 上传。 - 使用确定性的、不可猜测的文件名。不要保留原始文件名。使用 UUID 或文件内容的哈希。原始文件名会泄露信息 ("
passport_scan_2024_01_15.jpg"),可预测的名称使枚举成为可能。
签名 URL
签名 URL 是客户端访问私有桶的方式。过期时间、桶范围和被记录的内容都很重要。
- 签名 URL 默认过期时间为 1 小时或更短。Supabase JS SDK 的
createSignedUrl(path, expiresIn)接受秒数。绝不使用像31536000(一年) 这样的值 — URL 会变成永久的半公开链接。 - 绝不在数据库中存储签名 URL。在每次请求时在服务端生成新的。存储了带 1 年过期的签名 URL,如通过数据库导出泄露,就会授予长期访问。
- 记录签名 URL 的生成,而不仅是文件上传。如果之后怀疑有入侵,你需要知道谁、何时生成了哪个 URL。记录
auth.uid()+ 桶 + 对象路径 + 时间戳。 - 在提供用户上传的文件时使用
downloadAs选项。createSignedUrl(path, expiresIn, { download: '.jpg' })强制Content-Disposition: attachment标头,使文件下载而非渲染 — 击败 HTML / SVG / PDF 中嵌入 HTML 的执行类别。
运营卫生
存储配置会随时间漂移。这四项运营项目能让攻击面保持紧致。
- 每季度审计桶。仪表板 → Storage → Buckets。确认公共/私有状态和 MIME 类型列表与应用的预期一致。「临时」创建的桶如果无人删除就会变成永久的。
- 监控匿名列出操作。存储日志 (仪表板 → Logs → Storage) 记录
LIST请求。针对私有桶的匿名列出请求激增意味着有人正从外部探测它。 - 为短暂上传设置保留策略。临时桶 (图像预览、草稿上传) 应通过计划函数在 24-72 小时后自动删除。在 GDPR / CCPA 数据最小化义务下,无限期保留是一种责任。
- Run a FixVibe scan monthly. FixVibe reports storage buckets that anonymous visitors can list or read. New buckets get added; old ones change visibility — only continuous scanning catches the drift.
后续步骤
Run a FixVibe scan against your production URL — anonymously listable storage shows up as its own finding. Pair this checklist with Supabase RLS scanner for the table layer and Supabase service role key exposed in JavaScript for the key-exposure adjacency. For storage misconfigurations across other BaaS providers, see BaaS misconfiguration scanner.
