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 + SPA開発で「RLS」が必須な理由 〜チャットボット開発で学ぶセキュリティの落とし穴〜

Posted at

はじめに

「Supabaseって便利だよね、Firebase的に使えるし」
「フロントエンドから直接DBにアクセスできるの最高!」

...ちょっと待ってください。その設計、セキュリティ的に大丈夫ですか?

本記事では、実際のマルチテナント型AIチャットボットSaaSの開発経験をもとに、Supabase + SPAアーキテクチャでRLS(Row Level Security)がなぜ必須なのかを解説します。

TL;DR

  • Supabase + SPAでフロントエンドから直接アクセスする場合、APIキーがブラウザに露出する
  • RLSがないと、悪意あるユーザーが他社のデータを取得できる
  • バックエンド経由(プロキシ)パターンなら従来通りの設計でもOK
  • 直接アクセスパターンではRLSは必須のセキュリティ対策

アーキテクチャの3パターン

Supabase + SPAには、大きく3つのアーキテクチャパターンがあります。

パターン比較表

パターン APIキー露出 RLS必須 主な用途
1. バックエンド経由 ❌ 隠蔽 任意 従来型Webアプリ
2. SPA直接アクセス ⚠️ 露出 必須 Supabaseフル活用
3. ハイブリッド △ 一部露出 推奨 認証のみSupabase

パターン別の詳細

パターン1: 従来型(バックエンド経由 / リバースプロキシ)

特徴:

  • service_role キーはサーバー内に隠蔽
  • アクセス制御はバックエンドで実装
  • RLSは任意(多層防御として推奨)
  • 従来のWebアプリと同じセキュリティモデル

適しているケース:

  • 既存のバックエンドAPIがある
  • 複雑なビジネスロジックが必要
  • Supabaseをただのデータベースとして使用

パターン2: SPA直接アクセス(RLS必須)

特徴:

  • anon キーがブラウザで丸見え
  • アクセス制御はDB側(RLS)で実装が必須
  • バックエンドレスで開発可能
  • Supabaseの真価を発揮

適しているケース:

  • 素早くMVPを作りたい
  • バックエンドを持ちたくない
  • Supabase Authをフル活用

パターン3: ハイブリッド

特徴:

  • 認証はSupabase Auth(直接アクセス)
  • データアクセスはバックエンド経由
  • 両方のメリットを享受
  • 本記事のチャットボットはこのパターン

補足:認証もバックエンド経由にするケース

パターン3では認証をSupabase Authに直接アクセスしていますが、認証もバックエンド経由(リバースプロキシ) にするケースがあります。

認証をバックエンド経由にする理由

理由 詳細
外部認証との統合 SSO (SAML/OIDC)、社内Active Directory、他サービスのOAuth
セキュリティ要件 認証トークンをブラウザに一切露出させたくない
追加ビジネスロジック 認証時に監査ログ記録、利用規約同意チェック、カスタムMFA
統一API設計 認証もデータも同じAPIエンドポイント経由

判断基準:

Supabase Authだけで完結する?
├─ Yes → 直接アクセス(パターン3)でOK
└─ No  → バックエンド経由を検討
         ├─ 外部IdP (SSO/SAML) と統合したい
         ├─ 厳格なセキュリティポリシーがある
         └─ 認証時に複雑な追加処理が必要

なぜパターン2でRLSが必須なのか

ブラウザの開発者ツールで見ると...

パターン2では、ブラウザの開発者ツールを開くとSupabaseの接続情報が見えます:

// ブラウザのコンソールで誰でも確認できる
const supabase = createClient(
  'https://xxxxx.supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'  // anon key
)

悪意あるユーザーがこれを使って...

// 全企業のドキュメントを取得!
const { data } = await supabase
  .from('documents')
  .select('*')

console.log(data)
// [
//   { id: 1, organization_id: 'A社', content: '機密情報...' },
//   { id: 2, organization_id: 'B社', content: '競合他社のデータ...' },
//   { id: 3, organization_id: 'C社', content: '顧客リスト...' },
// ]

これは重大なセキュリティインシデントです。

パターン1なら安全?

パターン1(バックエンド経由)なら、service_role キーはサーバー内に隠蔽されているため、上記の攻撃は成立しません。

ただし、RLSを設定しておくと多層防御になるため、推奨されます

バックエンドのバグ → RLSが最後の砦として機能
SQLインジェクション → RLSで被害を自組織に限定

RLS(Row Level Security)とは

RLSは、PostgreSQLの機能で、行レベルでアクセス制御を行う仕組みです。

RLSポリシーの例

-- 1. RLSを有効化
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- 2. ポリシーを作成:自組織のデータのみ閲覧可能
CREATE POLICY "Users can view own organization documents"
  ON documents
  FOR SELECT
  USING (
    organization_id = (
      SELECT organization_id
      FROM memberships
      WHERE user_id = auth.uid()
      AND status = 'active'
    )
  );

これにより、SQLで全件取得しようとしても、自動的に自組織のデータのみがフィルタリングされます。

実例:マルチテナント型チャットボットSaaS

システム概要

私たちが開発しているのは、企業向けのRAG(Retrieval-Augmented Generation)チャットボットSaaSです。

採用アーキテクチャ

私たちはパターン3(ハイブリッド) を採用しています

  • 認証: Supabase Auth(フロントエンドから直接)
  • データアクセス: FastAPI経由(service_role使用)
  • RLS: 多層防御として有効化

実装パターン

パターン1:認証ユーザーのデータのみ(シンプル)

-- ユーザー自身のデータのみアクセス可能
CREATE POLICY "Users can access own data"
  ON user_settings
  FOR ALL
  USING (user_id = auth.uid());

パターン2:組織ベースのアクセス制御(マルチテナント)

-- ヘルパー関数を作成(再帰を避けるため SECURITY DEFINER)
CREATE OR REPLACE FUNCTION app.user_has_active_membership(org_id uuid)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
STABLE
AS $$
  SELECT EXISTS (
    SELECT 1 FROM memberships
    WHERE user_id = auth.uid()
    AND organization_id = org_id
    AND status = 'active'
  );
$$;

-- ポリシーで使用
CREATE POLICY "Users can view documents in their organization"
  ON documents
  FOR SELECT
  USING (app.user_has_active_membership(organization_id));

パターン3:ロールベースのアクセス制御(RBAC)

-- 管理者のみ編集可能
CREATE POLICY "Admins can update documents"
  ON documents
  FOR UPDATE
  USING (
    EXISTS (
      SELECT 1 FROM memberships
      WHERE user_id = auth.uid()
      AND organization_id = documents.organization_id
      AND role IN ('admin', 'owner')
    )
  );

キーの使い分け

キー 露出 RLS適用 用途
anon ブラウザに露出 ✅ 適用 未認証ユーザー
authenticated JWTに含まれる ✅ 適用 認証済みユーザー
service_role サーバー内のみ ❌ バイパス バックエンド処理

開発時のTips

1. RLSをバイパスしたい場合

開発やバッチ処理でRLSをバイパスしたい場合

# FastAPIバックエンドから
async with conn.transaction():
    # セッション変数でバイパス
    await conn.execute("SET LOCAL app.bypass_rls = 'true'")
    # ここでは全データにアクセス可能
    results = await conn.fetch("SELECT * FROM documents")

2. Supabase Dashboardでの確認

Supabase Dashboardはスーパーユーザー権限なので、RLSは自動的にバイパスされます。全データが見えるのは正常です。

3. RLSポリシーのデバッグ

-- 現在のユーザーIDを確認
SELECT auth.uid();

-- 現在のロールを確認
SELECT current_user, current_setting('role', true);

-- ポリシーが適用されているか確認
SELECT * FROM pg_policies WHERE tablename = 'documents';

よくある落とし穴

1. RLSを有効化しただけで安心

-- ❌ これだけではダメ!ポリシーがないと誰もアクセスできない
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- ✅ ポリシーも必要
CREATE POLICY "..." ON documents FOR SELECT USING (...);

2. FORCE ROW LEVEL SECURITY の使い忘れ

-- テーブル所有者もRLSを適用したい場合
ALTER TABLE documents FORCE ROW LEVEL SECURITY;

3. サービスロールキーをフロントエンドで使用

// ❌ 絶対にやってはいけない!
const supabase = createClient(url, 'service_role_key_ここに書いちゃダメ')

4. パターン1なのにRLSに頼りすぎ

バックエンド経由なのに、バックエンドでアクセス制御を実装せず
RLSだけに頼る → service_roleはRLSバイパスするので意味がない!

アーキテクチャ選択のフローチャート

チェックリスト

プロダクションにデプロイする前に確認

共通

  • service_role キーがフロントエンドに露出していないか
  • 環境変数でキーを管理しているか

パターン2(直接アクセス)の場合

  • 全テーブルでRLSが有効化されているか
  • 適切なポリシーが設定されているか
  • Supabase Security Advisorで警告がないか
  • 開発者ツールで他組織のデータが見えないか

パターン1/3(バックエンド経由)の場合

  • バックエンドで適切な認可チェックを実装しているか
  • RLSを多層防御として設定しているか(推奨)

まとめ

Supabase + SPAは開発効率が高い一方で、アーキテクチャパターンによってセキュリティ設計が大きく異なります

  • パターン2(直接アクセス): RLSは必須
  • パターン1/3(バックエンド経由): RLSは多層防御として推奨

自分のプロジェクトがどのパターンかを把握し、適切なセキュリティ対策を実装しましょう。


参考リンク


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?