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,將 Public bucket 開關設為關閉,除非你有明確理由 (行銷素材、無 PII 的公開頭像)。公開儲存桶會對讀取操作繞過 RLS — 任何知道儲存桶名稱的人都能列出與下載。
  2. 為每個儲存桶設定嚴格的檔案大小限制。儀表板 → Bucket settings → File size limit。50 MB 是使用者上傳的合理預設;對於影片 / 大檔案使用情境可刻意提高。沒有限制,單次惡意上傳就能耗盡你的儲存配額或月度頻寬。
  3. 限制每個儲存桶允許的 MIME 類型。允許的 MIME 類型清單 — 明確的白名單,而非黑名單。僅圖片儲存桶使用 image/jpeg、image/png、image/webp。在使用者內容儲存桶中絕不允許 text/html、application/javascript 或 image/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. 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.
  4. 若應用程式支援檔案編輯或刪除,加入 UPDATE 與 DELETE 策略。每個命令都需要自己的策略。跳過 DELETE 意味著已驗證使用者無法刪除自己的檔案;跳過 UPDATE 意味著檔案覆寫會靜默失敗。
  5. 在兩個瀏覽器工作階段中測試跨使用者存取。以使用者 A 登入、上傳檔案、複製路徑。在另一個瀏覽器以使用者 B 登入,嘗試透過 REST API 取得該檔案。回應必須是 403 或 404,絕對不能是 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. 在伺服器端從檔案的實際位元組重新檢查 MIME 類型,而非從 Content-Type 標頭。使用像 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. 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.

// 掃描你的 baas 介面

在別人之前找到開放的資料表。

輸入一個生產 URL。FixVibe 會列舉你的應用程式通訊的 BaaS 提供者、識別其公開端點,並回報未經驗證的用戶端可以讀取或寫入什麼。免費、無需安裝、不需信用卡。

  • 免費方案 — 每月 3 次掃描,註冊免信用卡。
  • 被動 BaaS 指紋識別 — 不需要網域所有權驗證。
  • Supabase、Firebase、Clerk、Auth0、Appwrite 等。
  • Coding-agent prompts where code/config applies, plus provider-console steps for hosted BaaS fixes.