AI生成コードのレビューは、上から順に読んではいけない。最初に見るのは「境界」——入力・認可・秘密・依存——であって、ロジックの正しさはその後だ。 なぜなら、AIが書いたコードが本番で漏れる原因は、ほぼ全てこの4つの境界に集中しているからだ。
私は一人 × 生成AI(Claude Code)で、経済産業大臣賞を受賞したB2B SaaS、本番二重課金0件の決済基盤、放送局向けのAIプラットフォームを作ってきました。生成の速度は手放せません。しかし——mergeボタンを押した瞬間、そのコードの作者は私になります。 git blame に残るのはAIの名前ではなく、私の名前だ。だから「AIが書いたから」は本番障害の言い訳になりません。書いていないコードを、自分の責任で出荷する。そのための技術が、レビューです。
本記事は、AI生成コードのdiffを開いて「最初に疑う場所」を先に見るレビュー手順を、実務から体系化します。「なぜAIが脆弱性を量産するのか」の原理と診断ツールの全体像はAI生成コードの脆弱性診断(4層診断)に、安全なコードの書き方のカタログはセキュアコーディングの実践(NIST SSDF / OWASP ASVS)に譲り、ここでは**「読む順番」と「疑う場所」**に集中します。
1. 「自分が書いていないコード」をレビューするのは、書くのとは別の技能だ
コードを書くとき、人は書きながら設計モデルを頭の中に積み上げます。「この値はここで検証済み」「この分岐はここで潰した」という前提が、手を動かすたびに更新される。だからバグに気づく。
ところがAIにコードを書かせると、その設計モデルが自分の中に無いまま、流暢な完成品だけが降ってきます。 ここに罠があります。AIの出力は——変数名は綺麗、コメントは丁寧、構造は整っている。読みやすい。 そして人間の脳は、読みやすさを「正しさ」や「安全さ」と取り違えます。これは研究でも裏付けられていて、Veracodeの2025年の調査では、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を開いたら、まず外部入力がどこから入り、検証を通らずにどこへ着くかを追います。
# 入力境界: 検証を通らず危険なシンクへ着く経路を洗う
rg -n "dangerouslySetInnerHTML|child_process|new Function\(" --type ts
rg -n "`SELECT .*\$\{|`.*\+ *req\.|query\(.*\+" --type ts # 文字列連結のSQL
ルートハンドラの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 / パストラバーサル)の潰し方そのものはセキュアコーディングの実践記事に譲ります。レビューで見るべきは一点——「この値、境界で検証されているか?」 です。
4. 認可境界 — 「認証はするが認可しない」を狙い撃つ
ここが最重要かつ最頻出です。AIは**認証(ログイン済みか)は書きます。しかし認可(その人がそのデータの持ち主か)**を平気で省きます。結果、ログインさえしていれば他人のリソースにアクセスできる——IDOR(オブジェクト単位の認可欠陥 / BOLA)が生まれます。これはOWASP API Security Top 10で2019年以来ずっと第1位の最頻出リスクです。
OSSのAegisの言葉を借りれば、セキュリティには**アプリ横断で一律に効く「水平の統制」と、「誰が何を所有するか」に依存する「縦のリスク」**があります。入力検証やヘッダーは水平——ライブラリが肩代わりできる。しかし認可は縦——あなたの事業のデータモデルに依存するので、ライブラリには直せません。 だからレビュアーが最も鋭く見るべき境界がここです。
diffで探すのは「idで引いているのに、所有者で絞っていない」パターンです。
# 認可境界: id で引いて所有者スコープが無い箇所を洗う
rg -n "findUnique\(|findFirst\(|\.eq\('id'" --type ts -A2 \
| rg -v "user_?id|owner|auth\.uid|session"
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句を読みます。
-- ❌ 認証はするが認可しない:ログイン済みなら全員の行が見える
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の検証観点に沿って挙げます。
USING (true)は「そのテーブルが本当に公開でよい」場合のみ正しい。 公開プロフィールや公開記事ならtrueは正解です。しかし請求書やチャットにtrueが付いていたら赤信号。「認証はするが認可しない」の典型なので、認証と認可の違い(所有者スコープ)を実コードで解説した記事で全パターンを確認してください。service_role経路には所有権チェックが要る。 Supabase公式ドキュメントの通り、service_roleキーはPostgreSQLのBYPASSRLSで動き、RLSを完全に無視します。守りの境界が「鍵の置き場所」と「サーバー側の明示的な所有権チェック」に移るので、service_roleを使うルートはownerId検証がコード側にあるかを必ず見ます。SECURITY DEFINER関数はsearch_pathを固定しているか。 固定しないと権限昇格の温床になります。
これは机上の話ではありません。AIビルダー製アプリのRLS未設定により、未認証で他人のデータを読み書きできた事例がCVE-2025-48757(CVSS 9.3 CRITICAL)として登録されています。そして規模感として——公開Supabaseアプリ1,000件の一次調査で、RLSを持つアプリのうち約9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた(公開リポジトリ標本ゆえ下限値。所有者非スコープ=即脆弱ではなく要確認)という結果があります。手法と全数はSupabase RLSの実地調査にまとめました。10件に1件近く、この境界に穴が開いている前提でレビューすべきです。
5. 秘密境界 — 鍵とenvの衛生
AIは、動かすことを優先して鍵を平気でハードコードします。また、Next.jsのように「サーバー/クライアントの境界」があるフレームワークでは、秘密をクライアントバンドルに漏らすミスをします。
# 秘密境界: 混入した鍵・トークンと、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層診断記事で詳説したので、ここでは繰り返しません)。
レビュー手順としては単純です。package.json の差分を、機能の差分と同じ真剣さで読むこと。
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 を1コマンドで挟みます。
npx @aegiskit/cli scan # インストール・設定不要。いまのdiffを静的解析する
正直に、これが何をして何をしないかを明記します。scan は——
- 水平の統制の欠落を自動で洗い出す(未検証の入力、混入した秘密、注入経路のtaint解析)。
- 縦のリスク(認可/IDOR・Supabase RLS設計)は「検出して警告」するだけ。あるテーブルが所有者非スコープでよいか=意図的な公開かは、事業ルールの意味の問題であり、ツールには判断できません。だからAegisは「完全に守る」とは決して言いません——そう謳うツールはむしろ危険です。
つまりscanは私のgrepチェックリストを機械化し、疑う場所を漏れなく提示するものであって、レビューを置き換えるものではありません。そしてこれをCIに固定すれば、同じ穴の再発を機械が止め続けます。CIでの品質ゲート化(型・テスト・静的解析・セキュリティの4層をどう強制するか)はAI駆動開発の品質ゲート設計にまとめました。
8. 人間にしか下せない判断 — 認可の「意味」と業務ロジック
機械が疑いを出したあと、最後に残るのは意味の判断です。ツールはポリシやコードの「形」を読みますが、あなたの事業ルールの「意味」は読めません。
- この
USING (true)は意図的な公開か、認可漏れか。 テーブルの目的を知る人間にしか答えられません。 - この状態遷移は二重に走っても安全か。 私が決済基盤で本番二重課金0件を達成できたのは、スキャナではなく、状態遷移と冪等性を人間の目で設計・レビューし切ったからです。「同じ支払いリクエストが2回来たらどうなるか」は、コードの形ではなく業務の意味の問題で、どんな静的解析も代わりに判断してくれません。
- この数量・価格・上限は悪用できないか。 業務ロジックの穴も同じく、仕様を握る人間の領域です。
だからレビューが機能する前提は、「正しい」の定義=受け入れ条件が言語化されていることです。仕様を先に固め、受け入れ条件をテストにするワークフローはSpec駆動開発の本番ワークフローに書きました。仕様が曖昧なら、レビュアーは「正しそう」以上の判断ができません。AIに実装を、人間に仕様決定と検証を——この役割分担が、レビューを可能にします。
9. diffを開いたら最初に見る順(チェックリスト)
最後に、私が実際にdiffを開いてから緑を出すまでの順序を、そのまま置きます。上から順に、被害半径の大きい境界からです。
① 入力境界 外部入力は境界で検証されているか(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時代のレビューの出発点です。