# AI生成コードのレビュー術 — 自分が書いていないコードを自分の責任で出荷する

> AIが生成したコードを『上から順に読む』レビューは効きません。diffを開いて最初に疑う4つの境界——入力・認可・秘密・依存——を先に見るレビュー手順を、一人×生成AIの実務から解説。grepで機械化する箇所と、人間にしか下せない認可判断の切り分け、npx @aegiskit/cli scan での自動化を示します。

- 公開日: 2026-07-07
- 著者: 友田 陽大
- タグ: バイブコーディング, AI駆動開発, セキュリティ, TypeScript
- URL: https://tomodahinata.com/blog/ai-generated-code-review-checklist-workflow-guide
- カテゴリ: バイブコーディング
- 総合ガイド: https://tomodahinata.com/blog/vibe-coding-what-is-tools-risks-production-guide

## 要点

- AI生成コードは上から読まない。diffを開いて最初に疑うのは入力・認可・秘密・依存の4境界で、ロジックの正しさはその後。本番で漏れる原因がこの4境界に集中しているから
- 流暢さは正しさではない。AIの出力は綺麗な完成品として降ってくるため、読みやすさが安全に見える。レビュアーの仕事は『正しそうか』ではなく『不正入力・同時アクセス・他人のIDでどう壊れるか』を問うこと
- 認可境界が最重要。『認証はするが認可しない』を狙い撃つ。idで引いて所有者で絞らないコード、RLSのUSING(true)、service_role経路の所有権チェック漏れが定番の穴
- 機械にできる疑いは機械へ。grepとnpx @aegiskit/cli scanで水平の統制を自動化しCIに固定。ただし縦のリスク（認可/RLS設計）は検出・警告どまりで、意図の判断は人間が握る
- 出荷の責任は最後の人間ゲートにある。blameに残るのは自分の名前。自分が書いていないコードを自分の責任で出す以上、境界とその意味の確認は委譲できない

---

**AI生成コードのレビューは、上から順に読んではいけない。最初に見るのは「境界」——入力・認可・秘密・依存——であって、ロジックの正しさはその後だ。** なぜなら、AIが書いたコードが本番で漏れる原因は、ほぼ全てこの4つの境界に集中しているからだ。

私は一人 × 生成AI（Claude Code）で、経済産業大臣賞を受賞したB2B SaaS、本番二重課金0件の決済基盤、放送局向けのAIプラットフォームを作ってきました。生成の速度は手放せません。しかし——**mergeボタンを押した瞬間、そのコードの作者は私になります。** `git blame` に残るのはAIの名前ではなく、私の名前だ。だから「AIが書いたから」は本番障害の言い訳になりません。書いていないコードを、自分の責任で出荷する。そのための技術が、レビューです。

本記事は、**AI生成コードのdiffを開いて「最初に疑う場所」を先に見る**レビュー手順を、実務から体系化します。「なぜAIが脆弱性を量産するのか」の原理と診断ツールの全体像は[AI生成コードの脆弱性診断（4層診断）](/blog/ai-generated-code-vulnerability-assessment-vibe-coding-security-guide)に、安全なコードの書き方のカタログは[セキュアコーディングの実践（NIST SSDF / OWASP ASVS）](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide)に譲り、ここでは**「読む順番」と「疑う場所」**に集中します。

---

## 1. 「自分が書いていないコード」をレビューするのは、書くのとは別の技能だ

コードを**書く**とき、人は書きながら設計モデルを頭の中に積み上げます。「この値はここで検証済み」「この分岐はここで潰した」という前提が、手を動かすたびに更新される。だからバグに気づく。

ところがAIにコードを書かせると、**その設計モデルが自分の中に無いまま、流暢な完成品だけが降ってきます。** ここに罠があります。AIの出力は——変数名は綺麗、コメントは丁寧、構造は整っている。**読みやすい。** そして人間の脳は、読みやすさを「正しさ」や「安全さ」と取り違えます。これは研究でも裏付けられていて、[Veracodeの2025年の調査](https://www.veracode.com/resources/analyst-reports/2025-genai-code-security-report/)では、AI生成コードの45%が既知のセキュリティ欠陥を含み、モデルが賢くなってもこのセキュリティ成績は横ばいでした。流暢さと安全さは相関しません。

だからレビュアーの問いは「これは正しそうに**見える**か」ではありません。問うべきは常にこれです——

> **不正な入力・同時アクセス・他人のIDを指す正規リクエストが来たとき、このコードはどう壊れるか。**

この問いは、コードを上から下へ読んでも答えが出ません。ハッピーパスの美しいロジックを追っている間に、集中力は尽き、肝心の境界に着く前にレビューが終わります。**読む順番を、境界から始める必要があります。**

---

## 2. 読む前に疑う — 4つの境界

AIが本番で作り込む穴は、驚くほど同じ場所に出ます。私はdiffを開いたら、ロジックより先に次の4境界を**この順で**探します。順番は「壊れたときの被害半径（blast radius）」の大きい順です。

| 境界 | AIがやりがちなミス | diffで最初に探すもの |
|---|---|---|
| **① 入力境界** | 外部入力を検証せずDB・シェル・HTMLへ直挿入 | 文字列連結のクエリ、`dangerouslySetInnerHTML`、Zodなしのハンドラ |
| **② 認可境界** | 認証はするが認可しない（他人のIDでも通る） | 所有者で絞らない`findUnique`、`USING (true)`のRLS |
| **③ 秘密境界** | 鍵のハードコード、クライアントへの秘密の漏出 | `sk_live`・`AKIA`・`process.env`の直参照、`NEXT_PUBLIC_`の誤用 |
| **④ 依存境界** | 存在しない/不要なパッケージの追加 | `package.json`の差分に増えた見覚えのない依存 |

「4境界を先に見る」——これがこの記事の全てです。以下、境界ごとに**diffで何をgrepし、何を良/悪と判断するか**を示します。

---

## 3. 入力境界 — 「この値はどこから来たか」を最初に問う

AIは「入力は正しい形で来る」という楽観のもとにコードを書きます。だから境界で検証せず、外部入力をそのまま危険なシンク（DB・シェル・HTML・URL）へ流します。

diffを開いたら、まず**外部入力がどこから入り、検証を通らずにどこへ着くか**を追います。

```bash
# 入力境界: 検証を通らず危険なシンクへ着く経路を洗う
rg -n "dangerouslySetInnerHTML|child_process|new Function\(" --type ts
rg -n "`SELECT .*\$\{|`.*\+ *req\.|query\(.*\+" --type ts   # 文字列連結のSQL
```

ルートハンドラのdiffでは、**入口にスキーマ検証があるか**を最初に見ます。無ければ、その下のロジックがどれだけ綺麗でも「未検証の値が全体に流れている」ということです。

```diff
 export async function POST(req: Request) {
-  const body = await req.json();               // 何でも通る
-  const user = await db.user.create({ data: body });
+  const body = ParsedInput.parse(await req.json()); // 境界で型と形を固定
+  const user = await db.user.create({ data: body });
   return Response.json(user);
 }
```

ポイントは、**検証を「境界」に集約する**ことです。ハンドラの奥で個別に`if`チェックを散らすのではなく、入口でZodスキーマに通し、以降は型で保証された値だけが流れる状態にする。個々の注入クラス（SQLi / XSS / SSRF / パストラバーサル）の潰し方そのものは[セキュアコーディングの実践記事](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide)に譲ります。レビューで見るべきは一点——**「この値、境界で検証されているか？」** です。

---

## 4. 認可境界 — 「認証はするが認可しない」を狙い撃つ

ここが最重要かつ最頻出です。AIは**認証（ログイン済みか）**は書きます。しかし**認可（その人がそのデータの持ち主か）**を平気で省きます。結果、ログインさえしていれば他人のリソースにアクセスできる——IDOR（オブジェクト単位の認可欠陥 / BOLA）が生まれます。これは[OWASP API Security Top 10で2019年以来ずっと第1位](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/)の最頻出リスクです。

OSSの[Aegis](/aegis)の言葉を借りれば、セキュリティには**アプリ横断で一律に効く「水平の統制」**と、**「誰が何を所有するか」に依存する「縦のリスク」**があります。入力検証やヘッダーは水平——ライブラリが肩代わりできる。しかし認可は縦——**あなたの事業のデータモデルに依存するので、ライブラリには直せません。** だからレビュアーが最も鋭く見るべき境界がここです。

diffで探すのは「**idで引いているのに、所有者で絞っていない**」パターンです。

```bash
# 認可境界: id で引いて所有者スコープが無い箇所を洗う
rg -n "findUnique\(|findFirst\(|\.eq\('id'" --type ts -A2 \
  | rg -v "user_?id|owner|auth\.uid|session"
```

```diff
 export async function GET(req: Request, { params }: { params: { id: string } }) {
-  // ❌ 認証はするが認可しない：ログイン済みなら「他人の文書」も返る
-  const doc = await db.document.findUnique({ where: { id: params.id } });
+  const { userId } = await requireSession(req);        // 認証
+  const doc = await db.document.findFirst({
+    where: { id: params.id, ownerId: userId },          // 認可＝所有者で絞る（縦の統制）
+  });
   return Response.json(doc);
 }
```

### Supabaseなら、RLSポリシのdiffを必ずSQLで読む

TypeScriptだけ見ても認可境界は分かりません。認可がSQLで書かれているからです。`supabase/migrations/**.sql` のdiffを開き、ポリシの`USING`句を読みます。

```sql
-- ❌ 認証はするが認可しない：ログイン済みなら全員の行が見える
create policy "read" on documents for select
  using ( auth.role() = 'authenticated' );

-- ✅ 行を所有者に絞る（縦の統制）
create policy "read own" on documents for select
  using ( auth.uid() = owner_id );
```

レビューの勘所を3つ、Aegisの検証観点に沿って挙げます。

1. **`USING (true)` は「そのテーブルが本当に公開でよい」場合のみ正しい。** 公開プロフィールや公開記事なら`true`は正解です。しかし請求書やチャットに`true`が付いていたら赤信号。「認証はするが認可しない」の典型なので、[認証と認可の違い（所有者スコープ）を実コードで解説した記事](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide)で全パターンを確認してください。
2. **`service_role` 経路には所有権チェックが要る。** [Supabase公式ドキュメント](https://supabase.com/docs/guides/database/postgres/row-level-security)の通り、`service_role`キーはPostgreSQLの`BYPASSRLS`で動き、RLSを完全に無視します。守りの境界が「鍵の置き場所」と「サーバー側の明示的な所有権チェック」に移るので、`service_role`を使うルートは`ownerId`検証がコード側にあるかを必ず見ます。
3. **`SECURITY DEFINER` 関数は `search_path` を固定しているか。** 固定しないと権限昇格の温床になります。

これは机上の話ではありません。AIビルダー製アプリのRLS未設定により、**未認証で他人のデータを読み書きできた事例が[CVE-2025-48757（CVSS 9.3 CRITICAL）](https://nvd.nist.gov/vuln/detail/CVE-2025-48757)として登録**されています。そして規模感として——**公開Supabaseアプリ1,000件の一次調査で、RLSを持つアプリのうち約9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた**（公開リポジトリ標本ゆえ下限値。所有者非スコープ＝即脆弱ではなく要確認）という結果があります。手法と全数は[Supabase RLSの実地調査](/blog/supabase-rls-security-field-study)にまとめました。10件に1件近く、この境界に穴が開いている前提でレビューすべきです。

---

## 5. 秘密境界 — 鍵とenvの衛生

AIは、動かすことを優先して鍵を平気でハードコードします。また、Next.jsのように「サーバー/クライアントの境界」があるフレームワークでは、**秘密をクライアントバンドルに漏らす**ミスをします。

```bash
# 秘密境界: 混入した鍵・トークンと、env の直参照を洗う
rg -n "sk_live|AKIA[0-9A-Z]{16}|-----BEGIN|ghp_[0-9A-Za-z]{36}" --type ts
rg -n "process\.env\.[A-Z_]+" --type ts        # 型なしの env 直参照
```

レビューで見る観点は2つです。

- **鍵はコードに存在してよいか。** 答えは常にNo。envか秘密管理へ。ハードコードされた鍵がdiffにあれば無条件でブロックし、しかも**git履歴から失効させる**必要があります（コミットに残った鍵は消しても履歴に生きています）。
- **`NEXT_PUBLIC_` の付け間違いはないか。** `NEXT_PUBLIC_` を付けた値はクライアントバンドルに焼き込まれます。ここに秘密鍵が入っていたら世界公開です。envは**型付きの境界モジュールを1つ通す**（`process.env`の直参照を禁止する）と、この事故を構造的に防げます。

---

## 6. 依存境界 — package.json の差分を必ず読む

AIはハルシネーション（幻覚）で**存在しないパッケージ名**を提案します。攻撃者がその幻覚名を先回り登録すると、AIの幻覚がそのままサプライチェーン攻撃になる——これが「slopsquatting」です（仕組みと防御は[4層診断記事](/blog/ai-generated-code-vulnerability-assessment-vibe-coding-security-guide)で詳説したので、ここでは繰り返しません）。

レビュー手順としては単純です。**`package.json` の差分を、機能の差分と同じ真剣さで読む**こと。

```bash
git diff package.json package-lock.json
npm view <新しく増えたパッケージ名>   # 実在するか・極端に新しくないか・DL数
```

見覚えのない依存が1つでも増えていたら、「それは本当に実在し、まともか」を人間が一度確認する。lockfileを信頼の唯一の源にし、CIでは`npm ci`で完全一致を強制する。**依存の増加は、機能ではなく攻撃面の増加として見る**——これがレビュアーの構えです。

---

## 7. レビューを機械に肩代わりさせる — grep と `npx @aegiskit/cli scan`

ここまでのgrepを毎回手で叩くのは続きません。**機械にできる疑いは、機械に肩代わりさせる**べきです。

入力境界・秘密境界・水平の統制（ヘッダー/CSP・レート制限・CSRF・型付きenv）は、静的解析で自動化できます。私はAI生成コードのレビューに、無料のOSS [Aegis](/aegis) を1コマンドで挟みます。

```bash
npx @aegiskit/cli scan   # インストール・設定不要。いまのdiffを静的解析する
```

正直に、これが**何をして何をしないか**を明記します。`scan` は——

- **水平の統制の欠落を自動で洗い出す**（未検証の入力、混入した秘密、注入経路のtaint解析）。
- **縦のリスク（認可/IDOR・Supabase RLS設計）は「検出して警告」するだけ**。あるテーブルが所有者非スコープでよいか＝意図的な公開かは、事業ルールの**意味**の問題であり、ツールには判断できません。だからAegisは「完全に守る」とは決して言いません——そう謳うツールはむしろ危険です。

つまり`scan`は**私のgrepチェックリストを機械化し、疑う場所を漏れなく提示する**ものであって、レビューを置き換えるものではありません。そしてこれをCIに固定すれば、同じ穴の再発を機械が止め続けます。CIでの品質ゲート化（型・テスト・静的解析・セキュリティの4層をどう強制するか）は[AI駆動開発の品質ゲート設計](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security)にまとめました。

---

## 8. 人間にしか下せない判断 — 認可の「意味」と業務ロジック

機械が疑いを出したあと、**最後に残るのは意味の判断**です。ツールはポリシやコードの「形」を読みますが、あなたの事業ルールの「意味」は読めません。

- **この`USING (true)`は意図的な公開か、認可漏れか。** テーブルの目的を知る人間にしか答えられません。
- **この状態遷移は二重に走っても安全か。** 私が決済基盤で**本番二重課金0件**を達成できたのは、スキャナではなく、状態遷移と冪等性を人間の目で設計・レビューし切ったからです。「同じ支払いリクエストが2回来たらどうなるか」は、コードの形ではなく業務の意味の問題で、どんな静的解析も代わりに判断してくれません。
- **この数量・価格・上限は悪用できないか。** 業務ロジックの穴も同じく、仕様を握る人間の領域です。

だからレビューが機能する前提は、**「正しい」の定義＝受け入れ条件が言語化されている**ことです。仕様を先に固め、受け入れ条件をテストにするワークフローは[Spec駆動開発の本番ワークフロー](/blog/spec-driven-development-claude-code-ai-agent-production-workflow)に書きました。仕様が曖昧なら、レビュアーは「正しそう」以上の判断ができません。**AIに実装を、人間に仕様決定と検証を**——この役割分担が、レビューを可能にします。

---

## 9. diffを開いたら最初に見る順（チェックリスト）

最後に、私が実際にdiffを開いてから緑を出すまでの順序を、そのまま置きます。上から順に、被害半径の大きい境界からです。

```text
① 入力境界   外部入力は境界で検証されているか（Zod等）。危険なシンクへの直挿入は無いか
② 認可境界   idで引いて所有者で絞っているか。RLSのUSING(true)は意図的公開か。
             service_role経路に所有権チェックはあるか
③ 秘密境界   鍵のハードコードは無いか。NEXT_PUBLIC_で秘密を漏らしていないか
④ 依存境界   package.jsonに見覚えのない依存が増えていないか。実在するか
⑤ 機械化     npx @aegiskit/cli scan を通し、疑いを機械にも洗わせる（CIに固定）
⑥ 意味の判断 認可の意図・状態遷移の冪等性・業務ロジックの悪用可否を人間が確認
⑦ ロジック   ①〜⑥が緑になって初めて、ハッピーパスの正しさを読む
```

順番が逆——つまりロジックを先に読み、境界を後回しにする——と、AI生成コードのレビューはほぼ確実に境界を見落とします。綺麗なロジックは最後でいい。**先に、壊れる場所を疑う。**

---

生成AIは、私の出荷速度を何倍にもしてくれます。しかし速度は、検証で相殺して初めて安全になります。そして検証の最後の一枚は、いつも人間のレビューです。**自分が書いていないコードを、自分の責任で出荷する。** そのために、diffを開いたら——ロジックではなく、境界から疑う。`git blame` に残るのは、私の名前だからです。

まずは手元のプロジェクトで `npx @aegiskit/cli scan` を1回。機械が洗える境界を洗い、人間は意味の判断に集中する。そこがAI時代のレビューの出発点です。
