# Supabaseの SECURITY DEFINER 関数の落とし穴 — search_path 未固定が RLS 迂回・権限昇格を生む

> SECURITY DEFINER関数は定義者権限で動くため、search_pathを固定しないと攻撃者が一時スキーマやpublicに同名オブジェクトを差し込み、RLSを迂回して権限昇格できます。set search_path = '' とスキーマ修飾、GRANT最小化という安全な書き方と、migrations/pg_procでの検出方法を実SQLで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Supabase, PostgreSQL, RLS, セキュリティ
- URL: https://tomodahinata.com/blog/supabase-security-definer-function-search-path-guide
- カテゴリ: アプリ層セキュリティ
- 総合ガイド: https://tomodahinata.com/blog/nextjs-supabase-application-security-guide

## 要点

- SECURITY DEFINER 関数は『定義者の権限』で実行される。Supabaseでは多くが postgres 所有のため、RPCとして呼ぶと RLS を飛び越える——SECURITY INVOKER（既定）との違いがRLS迂回の起点になる
- search_path を固定しないと、関数本体の『非修飾』のテーブル/関数参照が呼び出し側の search_path で解決される。攻撃者は一時スキーマ(pg_temp)や public に同名オブジェクトを差し込み、定義者権限で自分のコードを実行させられる（権限昇格）
- 安全パターンは3点セット——`set search_path = ''`（または信頼スキーマ＋pg_temp最後）に固定、本体のオブジェクトはすべてスキーマ修飾（public.foo・auth.uid()）、EXECUTE は PUBLIC から剥がし必要なロールにだけ GRANT
- 検出は機械化できる——migrations や `pg_proc` を走査し、`prosecdef = true` かつ search_path 未設定の関数を洗い出す（`npx @aegiskit/cli scan` やSupabaseのDatabase Linterでも）
- ただし『その関数を SECURITY DEFINER にすべきか』『誰に呼ばせるか』『本体の認可ロジックが正しいか』は設計判断。ツールは search_path 未固定を機械的に指摘できても、関数の権限設計の正しさは証明しない

---

最初に結論を述べます。**Supabaseのデータベース関数（RPC）が `SECURITY DEFINER` で定義され、かつ `search_path` を固定していないと、認証済みの一般ユーザーが「定義者（多くの場合 postgres）の権限」で任意のオブジェクトをすり替えられ、RLSを迂回して権限昇格できます。** これはSupabase固有のバグではありません。PostgreSQLの `SECURITY DEFINER` が昔から持つ古典的な落とし穴が、テーブルや関数を自動でREST/RPCとして公開するSupabaseという環境で、攻撃面として顕在化したものです。

行レベルセキュリティ（RLS）を完璧に張っても、その「外側」にこの穴は空きます。`SECURITY DEFINER` 関数は定義者の権限で動き、定義者がテーブル所有者ならRLSを飛び越えるからです。本記事は、(1) `SECURITY DEFINER` と `SECURITY INVOKER` の違い、(2) なぜ関数・RPCがRLSを迂回しうるのか、(3) `search_path` 未固定がどう攻撃に化けるのか、(4) 脆弱→修正の実SQL、(5) 検出の機械化、を一次情報と実コードで解説します。そして最後に正直な線引きをします——**この穴の「検出」は機械化できますが、「その関数の権限設計が正しいか」は人間の判断**です。これはアプリ層セキュリティ全体の地図の一部で、全体像は[Next.js × Supabase アプリケーションセキュリティ完全ガイド](/blog/nextjs-supabase-application-security-guide)に整理しています。

---

## 1. SECURITY DEFINER と SECURITY INVOKER —— 誰の権限で実行されるか

PostgreSQLの関数には、実行時に「誰の権限を使うか」を決める2つのモードがあります。

- **SECURITY INVOKER（既定）** —— 関数を**呼び出したユーザー**の権限で実行される。呼び出し元が `authenticated` なら、関数の中身も `authenticated` の権限で動く。
- **SECURITY DEFINER** —— 関数を**作成・所有するユーザー（定義者）**の権限で実行される。Unixの `setuid` プログラムと同じで、呼び出し元が誰であっても、中身は定義者の権限で走る。

公式ドキュメント（[PostgreSQL: CREATE FUNCTION](https://www.postgresql.org/docs/current/sql-createfunction.html)）はこう述べています——「`SECURITY DEFINER` 関数は、それを所有するユーザーの権限で実行されるため、誤用されないよう注意が必要だ」。つまり `SECURITY DEFINER` は、**意図的に権限の壁を越える**ための機能です。便利ですが、越える先が高権限（Supabaseでは多くの関数が `postgres` 所有）なら、設計を一つ誤るだけで権限昇格の踏み台になります。

| 観点 | SECURITY INVOKER（既定） | SECURITY DEFINER |
|---|---|---|
| 実行時の権限 | 呼び出したユーザー | 関数の定義者（所有者） |
| RLSの効き方 | 呼び出し元に対して効く | 定義者がテーブル所有者なら**飛び越える** |
| 典型用途 | 通常のクエリ・計算 | 一般ユーザーに権限を「貸す」処理（集計・通知・横断更新） |
| 危険度 | 低い（権限の拡大が無い） | **高い（権限の壁を越える）** |

`SECURITY DEFINER` 自体は悪ではありません。正当な用途もあります——「一般ユーザーには直接 `select` させたくないテーブルから、集計値だけを関数経由で返す」「監査ログのように、本人には更新させたくない行を関数の内側だけで追記する」といったケースです。鍵は、**権限を貸す範囲を関数の内側に閉じ込め、貸した先で攻撃者に主導権を渡さない**こと。`search_path` 未固定は、まさにこの「貸した先」で主導権を奪われる経路です。

重要なのは、**RLS迂回の起点はこの「権限の差」そのもの**だという点です。`SECURITY INVOKER` の関数なら、`search_path` を乗っ取られても呼び出し元の権限以上のことはできません。`SECURITY DEFINER` だからこそ、乗っ取りが「権限昇格」に化けます。だから最初の問いは常に「**この関数は本当に `SECURITY DEFINER` である必要があるか**」です（第5節で詳述）。

---

## 2. なぜ関数・RPC が RLS を「飛び越える」のか

Supabaseは、データベース関数を **PostgREST** 経由で自動的にRPCエンドポイントとして公開します。`anon` / `authenticated` ロールに `EXECUTE` 権限があれば、ブラウザから次のように呼べます。

```bash
# データベース関数 is_admin() を RPC として呼ぶ（anon キーだけで叩ける）
curl "https://<project>.supabase.co/rest/v1/rpc/is_admin" \
  -H "apikey: <anon-key>" \
  -H "Authorization: Bearer <user-jwt>" \
  -H "Content-Type: application/json" -d '{}'
```

ここでRLSとの関係が問題になります。PostgreSQLのRLSは「現在のユーザー」を基準に評価されますが、公式ドキュメント（[PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)）が明記するとおり、**テーブルの所有者は通常RLSをバイパスし**（`FORCE ROW LEVEL SECURITY` を明示しない限り）、**スーパーユーザーや `BYPASSRLS` 属性を持つロールは常にバイパス**します。

Supabaseのダッシュボードやマイグレーションで作った関数は、多くの場合 `postgres` が所有します。`postgres` は `public` スキーマのテーブルも所有しています。したがって——

> `SECURITY DEFINER` 関数（`postgres` 所有）の中で `public` のテーブルを触ると、その問い合わせは `postgres` として実行され、**RLSが効かない**。

これは、サーバー側で `service_role` キーを使ってRLSを飛び越えるのと**同じ構図**を、データベースの内側で作っていることになります。`service_role` キーの危険性とRLSバイパスについては[anonキーとservice_roleキーの露出](/blog/supabase-anon-key-service-role-key-exposure-guide)で扱っていますが、`SECURITY DEFINER` 関数は「鍵を漏らしていないのに、DBの中に同じバイパス経路を作ってしまう」点で見落とされやすい穴です。

```sql
-- 例：profiles に完璧な RLS を張っていても…
alter table public.profiles enable row level security;
create policy "read own profile" on public.profiles
  for select to authenticated using ( (select auth.uid()) = id );

-- この SECURITY DEFINER 関数経由なら、RLS を素通りして全行を集計できてしまう
create function public.count_all_profiles()
returns bigint language sql security definer as $$
  select count(*) from public.profiles;  -- postgres 権限＝RLS が効かない
$$;
```

`count_all_profiles()` 単体は「件数を返すだけ」で無害に見えます。しかし**「RLSが効かない実行コンテキスト」をRPCとして外に開けた**こと自体がリスクの本体です。次節で見るように、`search_path` が未固定だと、この実行コンテキストを攻撃者に乗っ取られます。

---

## 3. search_path 未固定という攻撃面

`search_path` は、**スキーマ修飾なしで書かれたオブジェクト名（テーブル・関数・型・演算子）を、どのスキーマから順に探すか**を決める設定です。`SECURITY DEFINER` 関数に `SET search_path` を付けないと、関数は**呼び出し側の `search_path` をそのまま継承**します。

ここに攻撃の余地が生まれます。`SECURITY DEFINER` 関数の本体が、オブジェクトを**非修飾**で参照していると、その名前が「攻撃者の用意した別物」に解決されうるのです。

### 3-1. 脆弱な関数：非修飾の参照を持つ SECURITY DEFINER

```sql
-- 脆弱：SECURITY DEFINER なのに search_path 未固定。本体は profiles を非修飾で参照
create function public.is_admin()
returns boolean
language plpgsql
security definer                 -- 定義者（postgres）権限で動く＝RLS を飛び越える
as $$
declare
  result boolean;
begin
  -- ↓ 非修飾の "profiles"。search_path 次第で別のオブジェクトに解決されうる
  select role = 'admin' into result
  from profiles
  where id = auth.uid();
  return coalesce(result, false);
end;
$$;
```

### 3-2. 攻撃：一時スキーマ・public に「同名の別物」を差し込む

攻撃者は認証済みの一般ユーザーで構いません。やることは、`is_admin()` の中の非修飾 `profiles` を、自分が作った同名オブジェクトにすり替えることです。

```sql
-- 攻撃者（authenticated）：一時テーブルで profiles を差し替える
create temp table profiles (id uuid, role text);
insert into profiles values (auth.uid(), 'admin');

-- pg_temp（一時スキーマ）は、非修飾のテーブル参照では既定で「最初」に探索される。
-- そのため is_admin() 内の "profiles" が攻撃者の一時テーブルに化け、
-- 定義者（postgres）権限で実行されている関数が "admin" を返す
select public.is_admin();   -- → true（本来 admin でないのに昇格）
```

なぜ一時テーブルが効くのか。ここはPostgreSQLの探索規則の中でも誤解されやすい点なので、正確に押さえます。

- **テーブル・ビュー・型などのリレーション名**：一時スキーマ `pg_temp` は、`search_path` に明示されていない場合、**既定で最初に探索されます**（`pg_catalog` よりも前）。そして一時テーブルの作成権限（`TEMP`）は既定で全ユーザーに付与されているため、**誰でもこの差し替えを実行できます**。これが最も普遍的に成立する攻撃ベクトルです。
- **関数・演算子名**：`pg_temp` からは解決されません。ただし、`search_path` に「攻撃者が `CREATE` 権限を持つスキーマ」（典型的には `public`）が含まれていれば、そこに同名関数を仕込んで乗っ取れます。

つまり `search_path` 未固定の `SECURITY DEFINER` 関数は、**「攻撃者が書き込めるスキーマに置いた同名オブジェクト」で、定義者権限のコードを乗っ取られる**のが本質です。`is_admin()` のように戻り値が認可判断に使われていれば権限昇格に、`SECURITY DEFINER` 関数の中で更新や `EXECUTE` を行っていれば任意処理の実行につながります。公式の[CREATE FUNCTION](https://www.postgresql.org/docs/current/sql-createfunction.html)も、この「信頼できないユーザーが書き込めるスキーマを探索経路に含めないこと」を `SECURITY DEFINER` の必須の注意点として挙げています。

---

## 4. 安全パターン —— 脆弱→修正の実SQL

防御の核心は、**「探索経路を呼び出し側に委ねない」かつ「オブジェクトを攻撃者がすり替えられないよう特定する」**の2点です。具体的には3つの対策を同時に適用します。

### 4-1. search_path を固定し、すべてをスキーマ修飾する（Supabase 推奨の最厳）

```sql
-- 修正：search_path を空に固定し、本体のオブジェクトをすべてスキーマ修飾する
create or replace function public.is_admin()
returns boolean
language plpgsql
security definer
set search_path = ''             -- ← 呼び出し側の search_path を無効化する
as $$
declare
  result boolean;
begin
  select role = 'admin' into result
  from public.profiles            -- ← スキーマ修飾。一時スキーマや public 差し込みに化けない
  where id = (select auth.uid()); -- ← auth.uid() も auth スキーマで修飾済み
  return coalesce(result, false);
end;
$$;
```

ここで**正直に補足すべき重要な点**があります。`set search_path = ''` は「魔法の盾」ではありません。空にすると、非修飾の名前は `pg_catalog`（組み込み関数）以外ほぼ解決できなくなり、**書き忘れがエラーとして表面化する**——これが狙いです。しかし前述のとおり、非修飾のリレーション名は空の `search_path` でも `pg_temp` が先に探索されます。したがって**実際に攻撃を無効化しているのは「スキーマ修飾」そのもの**であり、`set search_path = ''` は「修飾し忘れを黙って `public` に落とさず、必ず明示させる」ための土台です。`''` を付けただけで本体に非修飾参照が残っていれば、依然として乗っ取られえます。両者は必ずセットです。

`pg_catalog` は `search_path` を空にしても暗黙に探索されるため、`now()` や `lower()` などの組み込み関数は引き続き使えます。修飾が要るのは、`public`・`auth` など自前/拡張スキーマのオブジェクトです。

### 4-2. 代替：信頼スキーマに固定し、pg_temp を必ず最後に置く

`set search_path = ''` が厳しすぎる（既存の非修飾参照が多い）場合、PostgreSQL公式が示す作法は**「信頼できるスキーマに固定し、`pg_temp` を末尾に置く」**ことです。末尾に置けば、一時スキーマは最後にしか探索されず、リレーションのすり替えを無効化できます。

```sql
-- 代替：信頼スキーマを先に、pg_temp を最後に固定する（公式 CREATE FUNCTION の作法）
create or replace function public.is_admin()
returns boolean
language plpgsql
security definer
set search_path = public, pg_temp   -- 信頼スキーマ → pg_temp は必ず末尾
as $$ ... $$;
```

ただしこの形は、**`public` 自体が攻撃者に書き込み可能でないこと**が前提です。PostgreSQL 15以降は `public` への `CREATE` がデフォルトで `PUBLIC` に付与されなくなったため成立しやすくなりましたが、古い設定を引き継いだプロジェクトでは要確認です。迷ったら最厳の `set search_path = ''` ＋ 完全修飾を選ぶのが安全側です。

### 4-3. GRANT を最小化する —— 誰が呼べるかを絞る

`CREATE FUNCTION` は、既定で **`PUBLIC`（全ロール）に `EXECUTE` を付与**します。Supabaseでは、これがそのまま「anon でも叩けるRPC」になります。`SECURITY DEFINER` 関数では、まず暗黙の `EXECUTE` を剥がし、必要なロールにだけ与え直します。

```sql
-- GRANT 最小化：PUBLIC への暗黙の EXECUTE を剥がし、必要なロールにだけ許可する
revoke execute on function public.is_admin() from public;
grant execute on function public.is_admin() to authenticated;
-- anon（未ログイン）に開ける必要が無いなら、絶対に grant しない
```

これは「探索経路の固定」とは別軸の防御です。`search_path` を固定しても、**そもそも anon に開ける必要のない権限昇格関数を anon が叩ける**状態なら、攻撃面は広いままです。「誰の権限で動くか（DEFINER）」と「誰が呼べるか（GRANT）」は独立に最小化します。

### 4-4. そもそも SECURITY DEFINER が要るかを問う

最も効くのは、危険な機能を**使わないで済ませる**ことです。多くの関数は `SECURITY INVOKER`（既定）で十分で、その場合RLSは呼び出し元に対して正しく効き、`search_path` 乗っ取りも権限昇格になりません（呼び出し元の権限を超えられないため）。`SECURITY DEFINER` を選ぶのは、「一般ユーザーの権限では届かない処理を、限定的に肩代わりさせる」明確な理由があるときだけにします。PostgreSQL 15以降は `SECURITY INVOKER` を明示できるので、意図をコードに残しておくとレビューが楽になります。

---

## 5. Supabase で見かける危険な RPC パターン

実際の現場で繰り返し見かける形を挙げます。いずれも「動く」ので、デモやレビューをすり抜けます。

### パターンA：管理機能を SECURITY DEFINER で公開し、GRANT も搾れていない

```sql
-- 危険：ロール昇格を SECURITY DEFINER で公開。search_path 未固定 ＋ 既定で PUBLIC に EXECUTE
create function public.promote_to_admin(target uuid)
returns void
language sql
security definer            -- RLS を飛び越えて profiles を更新できる
as $$
  update profiles set role = 'admin' where id = target;  -- 非修飾＆所有権チェックなし
$$;
-- 既定の PUBLIC EXECUTE が残るため、anon/authenticated から rpc/promote_to_admin を叩ける
```

この関数には**3つの欠陥が同居**しています。(1) `search_path` 未固定（非修飾 `profiles`）、(2) `PUBLIC` への `EXECUTE` が残存、そして (3) **「呼び出し元が誰であれ、任意の `target` を admin にできる」という認可ロジックそのものの欠陥**です。

ここが分かれ道です。(1) と (2) は機械的に検出・修正できます。`set search_path = ''` を足し、`revoke ... from public` すればよい。しかし **(3) は `search_path` を完璧に固めても残ります。** 「この関数は誰が呼んでよく、誰を昇格してよいのか」——`update public.profiles set role = 'admin' where id = target and <呼び出し元が管理者である条件>` のような認可判断は、あなたの業務ルールにしか答えがありません。これが本記事の正直な核心です（第8節）。

### パターンB：RLSが効かない集計・横断取得を素通しで開ける

```sql
-- 危険：ダッシュボード用の横断集計を SECURITY DEFINER で公開（search_path 未固定）
create function public.org_dashboard(org uuid)
returns table(total int, revenue numeric)
language sql
security definer
as $$
  select count(*), coalesce(sum(amount), 0)
  from invoices where org_id = org;   -- 非修飾＆「呼び出し元がこの org に属するか」未検証
$$;
```

便利な「横断取得」ほど `SECURITY DEFINER` にされがちで、しかも `org` をクライアントから受け取って所有権を確かめない——これは関数の内側で起こす [IDOR（オブジェクト単位の認可欠陥）](/blog/nextjs-supabase-application-security-guide)です。修正は `search_path` 固定＋スキーマ修飾に加えて、本体に `and exists (select 1 from public.memberships where user_id = auth.uid() and org_id = org)` のような所有権条件を入れること。やはり**機械化できるのは前半（探索経路の安全化）まで**で、所有権条件の定義は設計です。

---

## 6. 検出 —— migrations と pg_proc の静的検証

「`SECURITY DEFINER` かつ `search_path` 未固定」という**形**は、機械的に洗い出せます。ここは自動化が本領を発揮する領域です。

### 6-1. 稼働中DBを直接調べる（pg_proc）

カタログ `pg_proc` には、関数が `SECURITY DEFINER` か（`prosecdef`）、どんな `SET` を持つか（`proconfig`）が記録されています。これを使えば一覧化できます。

```sql
-- SECURITY DEFINER 関数のうち search_path を固定していないものを洗い出す
select
  n.nspname  as schema,
  p.proname  as function,
  pg_get_userbyid(p.proowner) as owner,   -- 定義者＝この権限で動く
  p.proconfig as config                   -- SET 句。null なら呼び出し側を継承
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where p.prosecdef                          -- SECURITY DEFINER のみ
  and n.nspname not in ('pg_catalog', 'information_schema')
  and (
    p.proconfig is null
    or not exists (
      select 1 from unnest(p.proconfig) as cfg
      where cfg like 'search_path=%'
    )
  )
order by 1, 2;
```

これで「未固定の `SECURITY DEFINER` 関数」がゼロ件であることを、リリース前のチェックとして確認できます。SupabaseのDatabase Linterにも、同種を警告する `function_search_path_mutable` ルールがあります。

### 6-2. migrations を静的検証する（CIで番をさせる）

稼働DBに繋がなくても、`supabase/migrations/**.sql` を読めば同じ欠陥を見つけられます。RLS設定ミスの体系的な洗い出しは[RLS設定ミスの検出と監査](/blog/supabase-rls-misconfiguration-detection-audit-guide)にまとめていますが、`SECURITY DEFINER` 関数については特に次の3点をマイグレーションから機械的にフラグします。

- `security definer` を含むが、同じ定義に `set search_path` が無い
- `set search_path` はあるが、空でも `pg_temp` 末尾でもない（＝攻撃者書き込み可能なスキーマが先頭にありうる）
- 関数定義の後に `revoke execute ... from public` が無い（既定の `PUBLIC` EXECUTE が残存）

私が公開しているOSS Aegis は、`supabase/migrations` を解析してこれらを検出します。インストール不要で走ります。

```bash
# インストール不要・設定不要でスキャン（未固定の SECURITY DEFINER／過剰 GRANT を可視化）
npx @aegiskit/cli scan
```

正規表現だけだと `$$ ... $$` の本体や複数行定義で取りこぼすため、実用にはSQLをパースして「定義ブロック単位」で属性を見るのが確実です。重要なのは、**この一次フィルタをCIに常設し、未固定の `SECURITY DEFINER` が新たに混入したらビルドを止める**ことです。人間の記憶ではなく、機械に番をさせます。

---

## 7. 本番前チェックリスト

外注でもAI生成でも、`SECURITY DEFINER` 関数を本番に出す前に最低限これだけは確認してください。

- [ ] そもそも `SECURITY DEFINER` が必要か。`SECURITY INVOKER`（既定）で足りないかを先に問うた
- [ ] すべての `SECURITY DEFINER` 関数に `set search_path = ''`（または信頼スキーマ＋`pg_temp` 末尾）を**明示**している
- [ ] 関数本体のテーブル・型・関数は**すべてスキーマ修飾**（`public.foo`・`auth.uid()`）。非修飾参照がゼロ
- [ ] `set search_path = ''` を「付けただけ」で満足せず、修飾とセットになっている
- [ ] 関数定義の直後に `revoke execute ... from public` し、必要なロールにだけ `grant` している
- [ ] `anon`（未ログイン）に開ける必要のない関数を、`anon` から呼べる状態にしていない
- [ ] 関数本体に、呼び出し元の**所有権・権限を確かめる条件**が入っている（昇格・横断取得・更新系は特に）
- [ ] `pg_proc` クエリ（第6節）で「未固定の `SECURITY DEFINER`」がゼロ件であることを確認した
- [ ] migrations の静的検証（`npx @aegiskit/cli scan` 等）を**CIに常設**している

発注者・レビュアーの視点で最も効く質問は、**「この関数はなぜ `SECURITY DEFINER` なのですか？」「`search_path` は固定していますか？」「これは誰が呼べますか（anon でも叩けますか）？」**の3つです。設計を理解している開発者なら即答できます。

---

## 8. どこまで機械化でき、どこから設計か（正直に）

最後に、線を引きます。誇張は信頼を損なうので、できることとできないことを分けて述べます。

**機械化できること（検出・警告）。** 「`SECURITY DEFINER` かつ `search_path` 未固定」「非修飾参照の残存」「`PUBLIC` への過剰 `EXECUTE`」——これらは**形**の問題なので、`pg_proc` や migrations の静的検証で機械的に洗い出せます。OWASPの[Application Security Verification Standard（ASVS）](https://owasp.org/www-project-application-security-verification-standard/)が説く「セキュリティは『入れたか』ではなく『検証できるか』で測る」という規律に、この層はよく合致します。まずは [Aegis](/aegis)（無料OSS、`npx @aegiskit/cli scan`）で現状を可視化するのが、最もコスパの良い第一歩です。

**機械化できないこと（設計判断）。** 一方で、ツールが答えられない問いが残ります——「この関数は本当に `SECURITY DEFINER` であるべきか」「定義者の権限で何をどこまで肩代わりさせてよいか」「誰がこのRPCを呼んでよいか」「本体の認可ロジック（所有権・権限の条件）は正しいか」。第5節の `promote_to_admin` が示したとおり、`search_path` を完璧に固めても、**関数のロジックが『誰でも誰でも昇格できる』なら依然として脆弱**です。これらはあなたのデータモデルと業務ルールを理解した人間にしか判断できません。**いかなるツールも、`search_path` 未固定という罠は検出できても、関数の権限設計が『正しい』ことは証明しません。**「スキャンが通った＝よくある罠は踏んでいない」であって、「安全になった」ではない——この区別を曖昧にする製品は、むしろ油断を生みます。

だからこそ線引きが要ります。**どこまで自動の検出で固め、どこから人間のレビューが要るか。** `SECURITY DEFINER` 関数の権限設計や、既存Supabaseアプリの認可・RLSレビューが必要なら、[セキュリティ監査](/aegis/audit)で承ります。私自身、[木材流通業界のDX案件](/case-studies/lumber-industry-dx)で、RLS・テナント分離・権限境界を含むデータ層の認可を実運用で設計・検証してきました。検出の機械化は、人間が本当に難しい設計判断に集中するための土台です。

---

## よくある質問（FAQ）

**Q. `set search_path = ''` を付ければ、それで安全になりますか？**
A. それだけでは不十分です。空にする目的は「非修飾参照をエラーとして表面化させ、必ずスキーマ修飾を強制する」ことであり、実際に攻撃を無効化しているのは**スキーマ修飾そのもの**です。非修飾のリレーション名は空の `search_path` でも一時スキーマ（`pg_temp`）が先に探索されるため、`''` を付けても本体に非修飾参照が残れば乗っ取られえます。`''`（固定）と完全修飾は必ずセットにしてください。

**Q. すべての関数で `search_path` を固定すべきですか？**
A. `SECURITY DEFINER` 関数では必須です。`SECURITY INVOKER`（既定）でも固定する習慣は良い（挙動が呼び出し側に左右されなくなる）ですが、優先順位は圧倒的に `SECURITY DEFINER` が先。権限昇格に直結するのはそちらだからです。

**Q. RLSさえ正しく張れば、`SECURITY DEFINER` の心配は要りませんか？**
A. いいえ。`SECURITY DEFINER` 関数は定義者（多くは postgres＝テーブル所有者）の権限で動くため、**RLSを飛び越えます**。RLSは「呼び出し元の権限」に対して効くもので、定義者権限で実行されるコードには効きません。RLSとは独立に、関数側で `search_path` 固定・修飾・GRANT最小化・認可ロジックを担保する必要があります。

**Q. 一般ユーザーが本当に一時テーブルなんて作れるのですか？**
A. 作れます。一時オブジェクトの作成権限（`TEMP`）は、PostgreSQLでは既定で全ロールに付与されています。`public` への `CREATE` は新しい環境では絞られていることが多い一方、`pg_temp` 経由のリレーションすり替えは**より普遍的に成立する**ため、`SECURITY DEFINER` の主要な攻撃面として常に想定すべきです。

**Q. 既存の大量の関数を、どう棚卸しすればいいですか？**
A. まず第6節の `pg_proc` クエリで「未固定の `SECURITY DEFINER`」を全件抽出し、件数を把握します。次に `npx @aegiskit/cli scan` で migrations 側も突き合わせ、CIに常設して再発を止めます。修正は「固定＋修飾＋GRANT最小化」をテンプレ化すれば機械的に進みますが、各関数の**認可ロジックの妥当性**だけは1本ずつ人間が確認してください。

---

## まとめ：探索経路を呼び出し側に委ねない

要点を整理します。

- `SECURITY DEFINER` 関数は**定義者の権限**で動く。Supabaseでは多くが `postgres` 所有のため、RPCとして呼ぶと**RLSを飛び越える**。RLS迂回の起点は `SECURITY INVOKER`（既定）との「権限の差」そのもの。
- `search_path` を固定しないと、本体の**非修飾**のテーブル/関数参照が呼び出し側の探索経路で解決される。攻撃者は一時スキーマ（`pg_temp`）や `public` に同名オブジェクトを差し込み、定義者権限で自分のコードを実行させられる（**権限昇格**）。
- 安全パターンは**3点セット**——(1) `set search_path = ''`（または信頼スキーマ＋`pg_temp` 末尾）に固定、(2) オブジェクトを**すべてスキーマ修飾**、(3) `EXECUTE` を `PUBLIC` から剥がし必要なロールにだけ `grant`。`''` は修飾の強制装置であって、修飾とセットで初めて効く。
- 「未固定の `SECURITY DEFINER`」という**形**は、`pg_proc` や migrations の静的検証で**機械的に検出**でき、CIに番をさせられる。
- ただし「その関数を `SECURITY DEFINER` にすべきか」「誰に呼ばせるか」「本体の認可ロジックが正しいか」は**設計判断**。ツールは罠を検出できても、権限設計の正しさは証明しない。

AIで速く作ること自体は正しい。速く作ったSupabaseアプリのデータ層に潜む `SECURITY DEFINER` の落とし穴を検出・修正し、権限設計のレビューが必要であれば、お気軽にご相談ください。

---

## 参考資料

- [PostgreSQL — CREATE FUNCTION（SECURITY DEFINER と search_path の注意点）](https://www.postgresql.org/docs/current/sql-createfunction.html)
- [PostgreSQL — Row Security Policies（テーブル所有者・BYPASSRLS はRLSをバイパスする）](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- [Supabase Docs — Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
- [OWASP Application Security Verification Standard（ASVS）](https://owasp.org/www-project-application-security-verification-standard/)
