// docs / baas security / supabase storage
Supabase ストレージバケットセキュリティチェックリスト: 22 項目
Supabase Storage は S3 互換のバケットに加え、データベースと同じ行レベルセキュリティモデルをまとった薄いラッパーです。つまり、テーブルに影響する RLS の落とし穴が同様にファイルアクセスに影響し、AI コーディングツールがアップロードを実装する際に現れるストレージ特有の落とし穴もあります。このチェックリストは 5 つのセクションに渡る 22 項目です: バケット構成、RLS ポリシー、アップロード検証、署名 URL、運用衛生。各項目は 15 分以内に検証可能です。
下記の各項目は必須です。基盤となる RLS の仕組みについてはSupabase RLS スキャナを参照してください。ストレージに隣接するキー露出クラスについてはJavaScript に露出した Supabase サービスロールキーを参照してください。
バケット構成
正しいデフォルトから始めましょう。設定ミスのバケットは、RLS が正しくてもファイルを漏洩します。
- すべてのバケットをデフォルトでプライベートに。 Supabase ダッシュボード → Storage → Buckets で、明確な理由 (マーケティング素材、PII を含まない公開アバター) がない限り Public bucket トグルをオフに設定します。公開バケットは読み取り操作で RLS をバイパスします — バケット名を知る誰もが一覧表示およびダウンロードできます。
- すべてのバケットに厳密なファイルサイズ制限を設定。 Dashboard → Bucket settings → File size limit。ユーザーアップロードでは 50 MB が妥当なデフォルトです。動画 / 大容量ファイルのユースケースでは意図的に引き上げます。制限がなければ、単一の悪意あるアップロードでストレージクォータや月間帯域を使い果たせます。
- バケットごとに許可される MIME タイプを制限。 許可された MIME タイプリスト — ブロックリストではなく明示的な許可リスト。画像専用バケットでは
image/jpeg、image/png、image/webp。ユーザーコンテンツバケットではtext/html、application/javascript、image/svg+xmlを決して許可しないでください — 署名 URL 経由で配信されるとブラウザで実行されます。 - 共有された 1 つのバケットではなく、コンテンツタイプごとに 1 つのバケットを使用。 バケットごとの設定 (サイズ、MIME タイプ、RLS ポリシー) があなたの粒度です。
user-avatarsバケット、document-uploadsバケット、public-assetsバケットは、混在した 1 つのバケットよりもロックダウンしやすいです。 - フロントエンドがアップロードする場合は CORS 構成を確認。 ユーザーがブラウザから署名 URL に直接アップロードする場合、バケット CORS は本番オリジンをリストする必要があります。
*は公開バケットでのみ許容され、ユーザー PII を含むバケットでは決して許容されません。
storage.objects の RLS ポリシー
Supabase Storage はファイルメタデータを storage.objects テーブルに格納します。そのテーブルの RLS が、誰がファイルを読み、アップロードし、更新し、削除できるかを制御します。RLS がなければ、バケットの public/private フラグがあなたの唯一の保護です。
- 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 を省略するとファイル上書きは静かに失敗します。
- 2 つのブラウザセッションでクロスユーザーアクセスをテスト。 ユーザー 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 コーディングツールはデフォルトでクライアントのみの検証を生成します — それは何も保護しません。
- MIME タイプは
Content-Typeヘッダではなく、ファイルの実際のバイトからサーバー側で再チェックする。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(1 年) のような値は決して使用しないでください — URL が永続的な半公開リンクになります。 - 署名 URL をデータベースに保存しない。 リクエストごとにサーバー側で新しく生成します。データベースダンプ経由で漏洩した 1 年有効期限の保存済み署名 URL は、長期アクセスを与えます。
- ファイルアップロードだけでなく、署名 URL の生成もログに記録する。 後で侵害が疑われたとき、誰がいつどの URL を生成したかを知る必要があります。
auth.uid()+ バケット + オブジェクトパス + タイムスタンプを記録します。 - ユーザーアップロードファイルを提供する際は
downloadAsオプションを使用する。createSignedUrl(path, expiresIn, { download: '.jpg' })はContent-Disposition: attachmentヘッダを強制するため、ファイルはレンダリングではなくダウンロードされます — HTML / SVG / PDF 内 HTML の実行クラスを無効化します。
運用衛生
ストレージ構成は時間とともにドリフトします。これら 4 つの運用項目はサーフェスを引き締めます。
- 四半期ごとにバケットを監査。 Dashboard → Storage → Buckets。public/private 状態と MIME タイプリストがアプリの期待と一致することを確認します。「一時的に」作成されたバケットは、誰も削除しなければ恒久的になります。
- 匿名一覧操作を監視。 ストレージログ (Dashboard → 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.
