FixVibe

// docs / baas security / supabase storage

Supabase ストレージバケットセキュリティチェックリスト: 22 項目

Supabase Storage は S3 互換のバケットに加え、データベースと同じ行レベルセキュリティモデルをまとった薄いラッパーです。つまり、テーブルに影響する RLS の落とし穴が同様にファイルアクセスに影響し、AI コーディングツールがアップロードを実装する際に現れるストレージ特有の落とし穴もあります。このチェックリストは 5 つのセクションに渡る 22 項目です: バケット構成、RLS ポリシー、アップロード検証、署名 URL、運用衛生。各項目は 15 分以内に検証可能です。

下記の各項目は必須です。基盤となる RLS の仕組みについてはSupabase RLS スキャナを参照してください。ストレージに隣接するキー露出クラスについてはJavaScript に露出した Supabase サービスロールキーを参照してください。

バケット構成

正しいデフォルトから始めましょう。設定ミスのバケットは、RLS が正しくてもファイルを漏洩します。

  1. すべてのバケットをデフォルトでプライベートに。 Supabase ダッシュボード → Storage → Buckets で、明確な理由 (マーケティング素材、PII を含まない公開アバター) がない限り Public bucket トグルをオフに設定します。公開バケットは読み取り操作で RLS をバイパスします — バケット名を知る誰もが一覧表示およびダウンロードできます。
  2. すべてのバケットに厳密なファイルサイズ制限を設定。 Dashboard → Bucket settings → File size limit。ユーザーアップロードでは 50 MB が妥当なデフォルトです。動画 / 大容量ファイルのユースケースでは意図的に引き上げます。制限がなければ、単一の悪意あるアップロードでストレージクォータや月間帯域を使い果たせます。
  3. バケットごとに許可される MIME タイプを制限。 許可された MIME タイプリスト — ブロックリストではなく明示的な許可リスト。画像専用バケットでは image/jpegimage/pngimage/webp。ユーザーコンテンツバケットでは text/htmlapplication/javascriptimage/svg+xml を決して許可しないでください — 署名 URL 経由で配信されるとブラウザで実行されます。
  4. 共有された 1 つのバケットではなく、コンテンツタイプごとに 1 つのバケットを使用。 バケットごとの設定 (サイズ、MIME タイプ、RLS ポリシー) があなたの粒度です。user-avatars バケット、document-uploads バケット、public-assets バケットは、混在した 1 つのバケットよりもロックダウンしやすいです。
  5. フロントエンドがアップロードする場合は CORS 構成を確認。 ユーザーがブラウザから署名 URL に直接アップロードする場合、バケット CORS は本番オリジンをリストする必要があります。* は公開バケットでのみ許容され、ユーザー PII を含むバケットでは決して許容されません。

storage.objects の RLS ポリシー

Supabase Storage はファイルメタデータを storage.objects テーブルに格納します。そのテーブルの RLS が、誰がファイルを読み、アップロードし、更新し、削除できるかを制御します。RLS がなければ、バケットの public/private フラグがあなたの唯一の保護です。

  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. 2 つのブラウザセッションでクロスユーザーアクセスをテスト。 ユーザー 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 (1 年) のような値は決して使用しないでください — URL が永続的な半公開リンクになります。
  2. 署名 URL をデータベースに保存しない。 リクエストごとにサーバー側で新しく生成します。データベースダンプ経由で漏洩した 1 年有効期限の保存済み署名 URL は、長期アクセスを与えます。
  3. ファイルアップロードだけでなく、署名 URL の生成もログに記録する。 後で侵害が疑われたとき、誰がいつどの URL を生成したかを知る必要があります。auth.uid() + バケット + オブジェクトパス + タイムスタンプを記録します。
  4. ユーザーアップロードファイルを提供する際は downloadAs オプションを使用する。 createSignedUrl(path, expiresIn, { download: '.jpg' })Content-Disposition: attachment ヘッダを強制するため、ファイルはレンダリングではなくダウンロードされます — HTML / SVG / PDF 内 HTML の実行クラスを無効化します。

運用衛生

ストレージ構成は時間とともにドリフトします。これら 4 つの運用項目はサーフェスを引き締めます。

  1. 四半期ごとにバケットを監査。 Dashboard → Storage → Buckets。public/private 状態と MIME タイプリストがアプリの期待と一致することを確認します。「一時的に」作成されたバケットは、誰も削除しなければ恒久的になります。
  2. 匿名一覧操作を監視。 ストレージログ (Dashboard → 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