mcpField
S
supabase-security-basics

supabase-security-basics

github.com/jeremylongshoreセキュリティ2.9k69 回インストール2 日前に更新
リポジトリ

supabase-security-basics について

Supabase Security Basics は、Supabase プロジェクトの保護を支援するスキルです。API キーの分離、RLS ポリシー、API 強化を扱い、SQL と JavaScript の例を示します。

主な機能

  1. 01クライアント用とサーバー用のキー分離
  2. 02RLS ポリシーのパターンと監査
  3. 03API 強化チェックリスト
  4. 04SQL インジェクション対策の案内
  5. 05セキュリティエラーの診断

活用シーン

  1. 01public テーブルの RLS 適用状況の監査
  2. 02service role キーをサーバーコードに限定
  3. 03CORS とデータベース接続の強化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jeremylongshore/tons-of-skills-marketplace --skill supabase-security-basics

インストール後、Claude Code で/supabase-security-basicsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
errors.md
examples.md
implementation.md

詳細

Supabase セキュリティの基本

概要

Supabase は PostgREST を介して Postgres データベースをインターネットに直接公開します。Row Level Security が有効になっていないテーブルはすべて、プロジェクトの URL と anon キーの両方を知っている人なら誰でも、読み取りと書き込みが可能です。これらはいずれも公開されています。このスキルでは、Supabase セキュリティの3つの柱、つまりキーの分離(anon と service_role)、RLS ポリシーの適用、API サーフェスの強化について説明します。

前提条件

  • Dashboard にアクセスできる Supabase プロジェクト(ローカルまたはホスト型)が作成されていること
  • @supabase/supabase-js がインストールされていること(npm install @supabase/supabase-js)
  • SUPABASE_URL と SUPABASE_ANON_KEY の環境変数が設定されていること
  • SQL と Postgres の基礎知識

手順

ステップ 1 — 2つの API キーについて理解する

Supabase はプロジェクトごとに2つのキーを発行します。両者を混同することが、最もよくあるセキュリティ上のミスです。

キー環境変数クライアントに公開可能かRLS の動作
Anon キーSUPABASE_ANON_KEYはい — ブラウザーで安全すべての RLS ポリシーに従います
Service role キーSUPABASE_SERVICE_ROLE_KEY絶対に公開しないでくださいすべての RLS を回避します

anon キーは JWT であり、PostgREST はこれを使って適用する RLS ポリシーを判断します。RLS ポリシーで明示的に許可されたデータにしかアクセスできないため、クライアント側のバンドルに含めても安全です。service role キーはすべての RLS ポリシーを回避するため、サーバー側のコード(API ルート、Edge Functions、cron ジョブ、マイグレーションスクリプト)にのみ配置してください。

// CORRECT: anon key on the client
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// CORRECT: service role key ONLY in server-side code
// e.g., app/api/admin/route.ts (Next.js server route)
import { createClient } from '@supabase/supabase-js'

const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  { auth: { autoRefreshToken: false, persistSession: false } }
)
// WRONG — service role key in client-side code
// This bypasses ALL RLS and leaks your admin key to every user
const supabase = createClient(url, process.env.NEXT_PUBLIC_SERVICE_ROLE_KEY!)  // NEVER DO THIS

キーのローテーション: Dashboard > Settings > API でキーを再生成します。ローテーション後は、すべての環境変数を更新し、すべてのサービスを再デプロイしてください。古いキーは直ちに無効になり、猶予期間はありません。

ステップ 2 — すべてのテーブルで Row Level Security を適用する

RLS がなければ、public スキーマ内のテーブルはどれも、anon キーを持つ人なら誰でも REST API 経由で完全にアクセスできます。RLS は任意ではなく、主要なアクセス制御レイヤーです。

-- Audit: find tables missing RLS
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

-- Enable RLS on every public table
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- CRITICAL: enabling RLS with NO policies blocks ALL access via the API.
-- You MUST add at least one policy per table per operation (SELECT, INSERT, UPDATE, DELETE).

ポリシーパターン — ユーザーが自身の行を読み書きする場合:

-- SELECT: user can only read their own rows
CREATE POLICY "Users read own data"
  ON public.todos FOR SELECT
  USING (auth.uid() = user_id);

-- INSERT: user can only insert rows for themselves
CREATE POLICY "Users insert own data"
  ON public.todos FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- UPDATE: user can only update their own rows
CREATE POLICY "Users update own data"
  ON public.todos FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- DELETE: user can only delete their own rows
CREATE POLICY "Users delete own data"
  ON public.todos FOR DELETE
  USING (auth.uid() = user_id);

ポリシーパターン — 読み取りは公開、書き込みは認証済みユーザーのみの場合:

CREATE POLICY "Anyone can read posts"
  ON public.posts FOR SELECT
  USING (true);

CREATE POLICY "Authenticated users can insert"
  ON public.posts FOR INSERT
  WITH CHECK (auth.uid() IS NOT NULL);

ポリシーパターン — カスタム JWT クレームによるロールベースのアクセス制御:

-- Admin-only policy using app_metadata
CREATE POLICY "Admins have full access"
  ON public.settings FOR ALL
  USING (
    (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
  );

サーバー側でカスタムクレームを設定するには:

// Server-side only — requires service role key
const { error } = await supabaseAdmin.auth.admin.updateUserById(userId, {
  app_metadata: { role: 'admin' }
})

ポリシーパターン — 組織単位のアクセス制御:

CREATE POLICY "Org members can read projects"
  ON public.projects FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM public.members
      WHERE members.organization_id = projects.organization_id
      AND members.user_id = auth.uid()
    )
  );

重要な違い — USING と WITH CHECK:

  • USING (expr) — ユーザーが参照できる既存の行を絞り込みます(SELECT、UPDATE、DELETE)
  • WITH CHECK (expr) — 新規または変更された行のデータを検証します(INSERT、UPDATE)
  • UPDATE では両方が必要です。USING は対象にできる行を制御し、WITH CHECK は新しい値として許可される内容を制御します
ステップ 3 — API サーフェスを強化する

JWT の検証: Supabase はサーバー側で JWT を自動的に検証します。RLS ポリシー内の auth.uid() 関数は、検証済み JWT から認証済みユーザーの ID を取得します。RLS ポリシー内でトークンを手動で検証する必要はありません。Supabase が処理します。

SQL インジェクションの防止: Supabase JS SDK は内部でパラメーター化クエリを使用します。ユーザー入力から生の SQL 文字列を組み立てないでください。常に SDK のクエリビルダーを使用してください。

// SAFE: SDK parameterizes automatically
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('author_id', userId)
  .ilike('title', `%${searchTerm}%`)

// DANGEROUS: raw SQL with string interpolation
// Only use supabase.rpc() with parameterized functions, never template literals

ネットワーク制限: Dashboard > Settings > Database > Network Restrictions で、直接のデータベース接続を既知の IP 範囲に制限します。これは REST API(PostgREST 経由)には影響しませんが、Postgres への直接接続を保護します。

CORS の設定: Dashboard > Settings > API > CORS で、プロジェクトごとに許可するオリジンを設定します。デフォルトではすべてのオリジン(*)が許可されます。本番環境では自社のドメインに制限してください。

未使用の認証プロバイダーを無効にする: Dashboard > Authentication > Providers で、現在使用していないプロバイダー(メール、電話、Google、GitHub など)を無効にし、攻撃対象領域を減らします。

SSL の適用: Dashboard > Settings > Database > SSL Configuration で、すべての直接データベース接続に SSL を適用します。

ステートメントのタイムアウト: 長時間実行されるクエリによってデータベースリソースが枯渇するのを防ぎます。

ALTER ROLE authenticated SET statement_timeout = '10s';
ALTER ROLE anon SET statement_timeout = '5s';

デフォルトのスキーマ権限を取り消す(確認のみ):

-- Supabase handles this by default, but verify:
-- anon and authenticated roles should only access data through RLS policies
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
AND grantee IN ('anon', 'authenticated')
ORDER BY table_name, grantee;

出力

これらの手順を完了すると、次の状態になります。

  • クライアント側のコードでのみ使用する Anon キーと、サーバー側のみに制限されたサービスロールキー
  • すべてのパブリックテーブルで RLS が有効になり、操作ごとに明示的なポリシーが設定されている
  • ロールベースのアクセスパターンに対応するカスタム JWT クレームが設定されている
  • ネットワーク制限、CORS、SSL、ステートメントタイムアウトが強化されている
  • 未使用の認証プロバイダーが無効になっている
セキュリティ監査チェックリスト
  • すべてのパブリックテーブルで RLS が有効になっている(SELECT rowsecurity FROM pg_tables WHERE schemaname='public')
  • 各テーブルに、必要な操作ごとに少なくとも1つの RLS ポリシーが設定されている
  • サービスロールキーが、クライアント側や NEXT_PUBLIC_* 環境変数に含まれていない
  • .env ファイルが .gitignore に含まれている
  • メール確認が有効になっている(Dashboard > Authentication > Settings)
  • OAuth リダイレクト URL が自分のドメインに制限されている
  • 未使用の認証プロバイダーが無効になっている
  • SSL の強制が有効になっている(Dashboard > Database > SSL)
  • データベースのパスワードがデフォルトから変更されている
  • 直接 DB 接続に対するネットワーク制限が設定されている
  • authenticated ロールと anon ロールに statement_timeout が設定されている
  • 機密性の高いユーザー操作で MFA が有効になっている
  • 本番環境でポイントインタイムリカバリー(PITR)が有効になっている
  • 漏洩の疑いがある場合、API キーがローテーションされている

エラー処理

エラー原因解決方法
42501: new row violates row-level security policyRLS ポリシーがない、または WITH CHECK 条件に失敗しているその操作に対する RLS ポリシーを追加または修正し、auth.uid() が行のユーザー列と一致することを確認します
クエリがエラーなしで空の data を返すRLS の USING 句によってすべての行が除外されているポリシー内の auth.uid() が認証済みユーザーと一致することを確認し、JWT クレームを確認します
PGRST301: JWSErrorJWT トークンが無効または期限切れになっているユーザーを再認証し、SUPABASE_ANON_KEY がプロジェクトと一致することを確認します
PGRST302: anonymous access disabledクライアントの初期化時に Anon キーが渡されていないAnon キーを createClient() に渡し、環境変数が設定されていることを確認します
permission denied for table XRLS が有効だが、一致するポリシーがない特定の操作(SELECT/INSERT/UPDATE/DELETE)に対するポリシーを作成します
Could not find the function auth.uid()PostgREST コンテキスト外で SQL を実行しているauth.uid() は PostgREST が評価する RLS ポリシー内でのみ動作します。マイグレーションでは明示的なユーザー ID を使用します

例

新しいテーブルの最小限のセキュアなセットアップ:

-- 1. Create table
CREATE TABLE public.notes (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) NOT NULL DEFAULT auth.uid(),
  content TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- 2. Enable RLS immediately
ALTER TABLE public.notes ENABLE ROW LEVEL SECURITY;

-- 3. Add policies
CREATE POLICY "Users manage own notes" ON public.notes
  FOR ALL USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

クライアント側のクエリ(Anon キー — RLS による強制):

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// This only returns notes belonging to the authenticated user
// because the RLS policy filters by auth.uid()
const { data: notes, error } = await supabase
  .from('notes')
  .select('*')
  .order('created_at', { ascending: false })

リソース

次のステップ

  • supabase-prod-checklist を使って本番環境向けの強化を適用します
  • supabase-auth-flows を使って認証フローを設定します
  • supabase-migrations を使ってデータベースのマイグレーションを設定します

コメント