0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SupabaseとFirebaseでのセキュリティについて

0
Posted at

はじめに

モダンなWebアプリでは、こんな構成をよく見かけます。

  • フロント / ホスティング: Vercel (Next.js)
  • DB / 認証: Supabase
  • Auth / Storage / 通知: Firebase
  • バックエンド処理 / インフラ: GCP (Cloud Run, Cloud Functions)

サーバーレスで速く作れる一方、「どこに認可ロジックを置くか」を間違えると、個人情報が丸見えになります。

この記事では、

  1. なぜ「APIバイパス」が起きるのか
  2. DTOとRLSによる多層防御
  3. 各サービス層ごとの想定攻撃と防御
  4. GCPとAWSのIAM設計思想の違い
  5. Vercel特有の侵入経路

を整理します。

対象読者

  • Supabase / Firebase を使い始めたが、RLSやSecurity Rulesを後回しにしている人
  • BaaS構成でどこまで守ればいいか分からない人
  • 個人情報を扱うサービスをこれから作る人

1. 最初に理解すべき「APIバイパス」

多くの人が想定している構成

クライアント → 自作API (認証・認可・DTO変換) → Supabase → DB

「APIで認証チェックしてるから安全」——これが最大の落とし穴です。

実際に起きること

SupabaseのURLとanon keyは、フロントエンドのJSバンドルに含まれます。つまり 公開情報 です。

攻撃者はDevToolsを開いてキーを抜き、あなたのAPIを完全に無視してこう叩けます。

curl 'https://xxxxx.supabase.co/rest/v1/users?select=*' \
  -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6..." \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6..."
クライアント ─┐
              │  ← ここを飛ばされる
   自作API ───┤
              ↓
         Supabase → DB  ← 直接叩かれる

これが APIバイパス です。

API層に置いた認可ロジックは、API層を通らなければ意味がありません。
Firebaseも同様で、Firestoreへはクライアントから直接アクセスできます。

結論: 鍵は漏れる前提で設計する。最終防衛ラインはDB層に置く。


2. DTO と RLS —— 役割の違い

DTO RLS
アプリケーション (API境界) データベース (PostgreSQL)
守るもの どの列を返すか」 どの行を返すか」
バイパス されうる されない
役割 過剰な情報露出の防止 不正アクセスの遮断

2-1. DTO: 列レベルの制御

エンティティをそのまま返さず、用途ごとにスキーマを分けます。

// テーブルの実体(絶対にそのまま返さない)
type UserRow = {
  id: string;
  email: string;
  display_name: string;
  phone_number: string;
  ssn: string;            // 機密
  bank_account: string;   // 機密
  role: string;
  created_at: string;
};

// 公開用
export type UserPublicDTO = Pick<UserRow, 'id' | 'display_name' | 'created_at'>;

// 本人用
export type UserPrivateDTO = UserPublicDTO & Pick<UserRow, 'email' | 'phone_number'>;
export const UserMapper = {
  toPublic(u: UserRow): UserPublicDTO {
    return { id: u.id, display_name: u.display_name, created_at: u.created_at };
  },
  toPrivate(u: UserRow): UserPrivateDTO {
    return { ...UserMapper.toPublic(u), email: u.email, phone_number: u.phone_number };
  },
};

select('*') してから絞るのではなく、そもそもSELECTする列を絞る方が安全です。
DTOは「万一取得してしまった時の最後のフィルタ」と考えるとよいです。

2-2. RLS: 行レベルの制御(こちらが本命)

-- まずRLSを有効化(これを忘れると全公開)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- 自分の行だけ読める
CREATE POLICY "read_own_user"
  ON users FOR SELECT
  USING (auth.uid() = id);

-- 自分の行だけ更新できる
CREATE POLICY "update_own_user"
  ON users FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);

-- 削除は禁止(論理削除のみ許可する運用)
CREATE POLICY "deny_delete"
  ON users FOR DELETE
  USING (false);

RLSを有効にすると、先ほどのcurl直撃は 空配列 を返すようになります。エラーではなく「行が存在しない」扱いになるのがポイントです。

ENABLE ROW LEVEL SECURITY を忘れたテーブルは、anon keyで全世界に公開されています。
Supabaseのダッシュボードで「Unrestricted」バッジが出ているテーブルは即対応してください。

2-3. 組み合わせた多層防御

┌──────────────────────────────┐
│  クライアント                 │
└───────────┬──────────────────┘
            │
      ┌─────▼─────┐        ┌────────────────┐
      │ API層     │        │ 攻撃者は        │
      │ + DTO変換 │        │ ここを飛ばせる  │
      └─────┬─────┘        └───────┬────────┘
            │                      │
      ┌─────▼──────────────────────▼────┐
      │  Supabase RLS  ← 最終防衛ライン  │
      └─────┬───────────────────────────┘
            │
      ┌─────▼─────┐
      │PostgreSQL │
      └───────────┘
攻撃 防ぐ層
APIをバイパスして直接クエリ RLS
APIの実装ミスで機密列を返却 DTO
認可チェック漏れのエンドポイント RLS

3. Firebase: 想定攻撃と防御

サービス 想定攻撃 防御
Authentication メールアドレス列挙(存在確認APIの悪用) Email Enumeration Protection を有効化
IDトークンのリプレイ トークン有効期限を短く、サーバー側で毎回検証
匿名認証で作ったUIDを正規ユーザー扱い 匿名→正規の昇格はサーバー側で明示的に処理
Firestore / RTDB Security Rules未設定で全read/write デフォルトDENY。allow read: if request.auth != null を最低ライン
if true の書き残し Rules Playground + エミュレータでルールのユニットテスト
ネストしたサブコレクションの権限漏れ ワイルドカード{doc=**}の影響範囲を必ず確認
Cloud Storage 誰でもアップロード可能 → ストレージ枯渇/違法コンテンツ設置 Storage Rulesでサイズ・Content-Type・認証を検証
署名付きURLの長期発行 有効期限は分〜時間単位
Cloud Functions HTTPトリガーが無認証で公開 Callable Functionを使い context.auth を必須チェック
共通 Bot / スクレイピングによる大量リクエスト App Check を導入(正規アプリからのリクエストのみ許可)

Firestore Rulesの最小構成例:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // デフォルトで全拒否
    match /{document=**} {
      allow read, write: if false;
    }

    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow update: if request.auth != null
                    && request.auth.uid == userId
                    // roleの自己昇格を防ぐ
                    && !request.resource.data.diff(resource.data).affectedKeys()
                         .hasAny(['role']);
      allow delete: if false;
    }
  }
}

4. GCP: 想定攻撃と防御

サービス 想定攻撃 防御
Cloud Run / Functions --allow-unauthenticated のまま公開放置 --no-allow-unauthenticated + IAM invoker で制御
SSRF → メタデータサーバー(169.254.169.254)からトークン窃取 外部URLを受け取る処理はallowlist化、VPC-SC
IAM サービスアカウントに Editor / Owner を付与 最小権限。必要ならカスタムロール
SAのJSONキーがGitにコミットされる キーレス化(Workload Identity Federation)
Secret Manager 権限が広く誰でもシークレット取得可 roles/secretmanager.secretAccessor を対象SAのみに
Cloud Storage バケットが allUsers に公開 Organization Policy で Public Access Prevention を強制
VPC / Firewall 0.0.0.0/0 全開放 必要ポートのみ。内部通信はPrivate Google Access
監査 侵入に気づけない Cloud Audit Logs 有効化 + 異常検知アラート

特に効くのは Workload Identity Federation です。
VercelやGitHub ActionsからGCPを叩く際、SAキーJSONを配らずに済むため「鍵の流出」という事故カテゴリごと消せます。


5. GCP と AWS の IAM、何が違うのか

同じ「IAM」という名前ですが、設計思想がかなり違います。

観点 GCP IAM AWS IAM
付与の考え方 リソース階層(組織→フォルダ→プロジェクト)にロールをバインド プリンシパル(ユーザー/ロール)にポリシーをアタッチ
記述形式 事前定義ロール中心 (roles/storage.objectViewer) JSONポリシーを自分で記述
継承 上位階層から自動継承。構造が直感的 Organizations + SCP。継承より「上限の設定」に近い
Deny Deny Policy(比較的新しい機能) Explicit Deny が常に最優先(古くから強力)
一時権限 Workload Identity Federation(キーレス) STS AssumeRole(一時クレデンシャル)
粒度 ロール単位でシンプル。IAM Conditions で条件付与 ステートメント単位で極めて細かい

まとめると

  • GCP: 「階層 × 事前定義ロール」でシンプルだが、継承による意図しない権限拡大に注意
  • AWS: 「JSONで自由自在」だが複雑さゆえに過剰権限を作りやすい

どちらにせよ事故の9割は「とりあえず広い権限を付けた」なので、最小権限 + 定期的な権限棚卸しが本質です。


6. Vercel: 侵入経路とリスク

経路 リスク 対策
NEXT_PUBLIC_ 誤用 Service Role Key等がJSバンドルに埋め込まれ全世界公開 機密値はprefixなしのサーバー専用変数に
API Routes / Edge Functions 認証チェック漏れのエンドポイントが誰でも叩ける 各Route冒頭でセッション/JWT検証を必須化
Preview Deployments PRごとの公開URLが本番と同じDBを参照 Deployment Protection(パスワード/SSO)、Preview用に別環境の変数を設定
CORS誤設定 Allow-Origin: * で任意オリジンからAPI利用可 オリジンをallowlist化
ビルドログ / リポジトリ ログやコミットに環境変数が残る .env.gitignore、変数はDashboardで管理
npm依存 サプライチェーン攻撃でビルド時に環境変数が外部送信される lockfile固定、npm audit、依存の追加を審査
Rate Limit未設定 認証エンドポイントへの総当たり Vercel Firewall / WAF、Rate Limiting

Vercelで一番見落とされがちなもの

Preview Deployments です。

main   → https://myapp.vercel.app          (守っている)
PR #42 → https://myapp-git-feat-xxx.vercel.app  (URLを知っていれば誰でも見られる)

Preview環境が本番のSupabase/Firebaseを向いていると、被害範囲は本番と同等です。
必ず Deployment Protection を有効にするか、Preview専用の環境変数を設定してください。


7. この構成での優先順位

全部やるのが理想ですが、リソースが限られるなら次の順です。

  1. Supabase RLS を全テーブルで有効化 —— anon keyは公開情報なので、これが無いと素通し
  2. Firebase Security Rules をデフォルトDENYに —— 同上
  3. GCP IAM の最小権限化 + キーレス化 —— 侵入後の被害範囲を限定
  4. Vercel の Preview保護 と 環境変数の棚卸し —— 事故の入口を塞ぐ
  5. DTO層の整備 —— 情報露出をさらに絞る
  6. 監査ログとアラート —— 気づける状態にする

チェックリスト

### Supabase
- [ ] 全テーブルで RLS が有効(Dashboardに「Unrestricted」が無い)
- [ ] SELECT / INSERT / UPDATE / DELETE それぞれにポリシーを定義
- [ ] Service Role Key はサーバーサイドのみで使用
- [ ] RPC (SECURITY DEFINER) の権限昇格を確認

### Firebase
- [ ] Security Rules がデフォルトDENY
- [ ] Rules のユニットテストがCIで回っている
- [ ] App Check を導入
- [ ] Storage のサイズ / Content-Type 制限

### GCP
- [ ] SAに Owner / Editor を付けていない
- [ ] SAキーJSONを配布していない(Workload Identity)
- [ ] Cloud Run / Functions が無認証公開になっていない
- [ ] Audit Logs 有効 + アラート設定

### Vercel
- [ ] NEXT_PUBLIC_ に機密値が入っていない
- [ ] Preview Deployment に保護がかかっている
- [ ] API Routes 全てで認証チェック
- [ ] Rate Limiting 設定済み

おわりに

BaaS構成のセキュリティは、**「認可をどこに置くか」**の一点に尽きます。

  • API層の認可はバイパスされうる
  • だから RLS / Security Rules(DB層)が最終防衛ライン
  • DTOやIAMは、それを補強する多層防御

「anon keyは漏れる前提」——ここから設計を始めれば、大きな事故はかなり減らせるはずです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?