AIが書いたコードは「動く」が「安全」ではない
2025年12月、セキュリティ企業Tenzaiが衝撃的なレポートを公開しました。Claude Code、OpenAI Codex、Cursor、Replit、Devinの5つのAIコーディングエージェントに同一仕様のWebアプリケーションを3つずつ構築させ、合計15アプリのセキュリティ監査を実施した結果、69件の脆弱性が検出されたのです。
中でも致命的だったのは、15アプリ全てでCSRF対策がゼロだったという事実です。セキュリティヘッダ(CSP、HSTS、X-Frame-Options、CORS)を設定したアプリも皆無でした。
さらにCarnegie Mellon大学の研究チーム(SUSVIBES)は、AIコーディングエージェントが実装した機能のうち61%は機能的に正しく動作するものの、セキュアだったのはわずか10.5%だったと報告しています。つまり「動くコード」の約8割が脆弱性を抱えている計算です。
本記事では、AIコーディングツールがなぜセキュリティを見落とすのか、その技術的メカニズムを掘り下げ、実務で使える対策を提示します。
参考: Tenzai - Bad Vibes: Comparing the Secure Coding Capabilities of Popular Coding Agents / Carnegie Mellon SUSVIBES研究
CSRFトークン漏洩 ── 全ツールが見落とした具体例
Tenzaiのテストでは、各AIエージェントにソーシャルメディア風のアプリ、銀行取引アプリ、タスク管理アプリを構築させました。いずれも認証付きのフォーム送信を含む、CSRF対策が必須のアプリケーションです。
銀行取引アプリのケース(Express.js + EJS)
銀行取引アプリはNode.js(Express.js)+ EJSテンプレートで構築されました。AIエージェントが生成した送金エンドポイントは、典型的には以下のようなコードでした。
// AIが生成した脆弱なコード(Express.js)
app.post('/transfer', isAuthenticated, async (req, res) => {
const { toAccount, amount } = req.body;
// セッションからログインユーザーを取得
const fromUser = req.session.userId;
// 残高チェックと送金処理
await db.query(
'UPDATE accounts SET balance = balance - $1 WHERE user_id = $2',
[amount, fromUser]
);
await db.query(
'UPDATE accounts SET balance = balance + $1 WHERE account_number = $2',
[amount, toAccount]
);
res.redirect('/dashboard');
});
このコードは機能的には正しく動作します。認証チェック(isAuthenticatedミドルウェア)も入っています。しかし、CSRFトークンの検証が一切ありません。攻撃者は以下のようなHTMLを自分のサイトに仕込むだけで、ログイン中のユーザーの口座から送金を実行できます。
<!-- 攻撃者のサイトに設置されるCSRF攻撃用ページ -->
<html>
<body onload="document.getElementById('csrf-form').submit();">
<form id="csrf-form" action="https://victim-bank.example.com/transfer" method="POST">
<input type="hidden" name="toAccount" value="attacker-account-123" />
<input type="hidden" name="amount" value="100000" />
</form>
</body>
</html>
正しい実装では、csurfミドルウェア(またはExpress 5以降であればcsrf-csrf等の後継ライブラリ)を導入し、トークンの生成・検証を行う必要があります。
// 修正後のセキュアなコード
const { doubleCsrf } = require('csrf-csrf');
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: '__csrf',
cookieOptions: { sameSite: 'strict', secure: true, httpOnly: true },
});
// 全POST/PUT/DELETEにCSRF検証ミドルウェアを適用
app.use(doubleCsrfProtection);
// フォーム表示時にトークンを生成してテンプレートに渡す
app.get('/transfer', isAuthenticated, (req, res) => {
const csrfToken = generateToken(req, res);
res.render('transfer', { csrfToken });
});
// EJSテンプレート側
// <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
app.post('/transfer', isAuthenticated, async (req, res) => {
// doubleCsrfProtectionミドルウェアがトークンを自動検証
// 不正なリクエストはここに到達する前に403で弾かれる
const { toAccount, amount } = req.body;
// ... 送金処理
});
ポイントは、CSRF対策が単一のファイルで完結しないことです。ミドルウェアの設定、テンプレートへのトークン埋め込み、Cookieの設定という3つのレイヤーが連携する必要があります。AIエージェントは「送金フォームを作って送信を処理する」という機能要件に忠実に応えましたが、この横断的な防御策を自発的に追加しませんでした。
ツールごとの脆弱性件数と特徴
Claude Codeは5ツール中最多の16件の脆弱性を生み出し、さらに他のツールには見られなかった2FA無効化バイパスまで導入していました。具体的には、2FA設定変更エンドポイントに再認証を要求しなかったため、セッションハイジャック後に2FAを無効化できる状態でした。Codex、Cursor、Replitはそれぞれ13件。Devinは14件です。
全5ツールがSSRF(Server-Side Request Forgery)もURL Preview機能に導入しており、内部サービスへの不正アクセス、ファイアウォールのバイパス、認証情報の漏洩が可能な状態でした。
参考: AI coding agents keep repeating decade-old security mistakes / Vibe Coding Tested: AI Agents Nail SQLi but Fail Miserably on Security Controls
なぜAIツールはセキュリティ脆弱性を見逃すのか
訓練データの偏り ── セキュアなコードは少数派
LLMの訓練データの大部分は、GitHubの公開リポジトリ、Stack Overflow、技術ブログです。ここに構造的な問題があります。
GitHubの公開リポジトリの大半は個人プロジェクトや学習用コードであり、商用レベルのセキュリティ対策が施されたコードは少数派です。2023年のGitHub調査では、公開リポジトリの約40%がシークレット(APIキー、パスワード等)をコード内にハードコードしていたと報告されています。Stack Overflowの回答はさらに顕著で、「Express.jsでPOSTリクエストを処理する方法」の回答にCSRF対策やレート制限が含まれていることはまずありません。回答者は質問の範囲に絞って最小限のコードを提示するため、セキュリティは意図的に省略されます。
つまり、訓練データにおけるセキュアなコードとインセキュアなコードの比率は、圧倒的にインセキュア側に偏っています。LLMはこの分布を学習するため、「最もありそうなコード」を生成した結果、セキュリティが欠落するのは統計的な必然です。
Transformerのattentionメカニズムとセキュリティの相性
LLMの基盤であるTransformerアーキテクチャのattentionメカニズムも、セキュリティの見落としに寄与していると考えられます。
attentionは、入力プロンプト内のトークン間の関連性をスコアリングし、次のトークン生成に最も関連するコンテキストに重みを集中させます。「送金フォームを作成してください」というプロンプトに対して、attentionは「フォーム」「送金」「POST」「データベース更新」といった機能実装に直結するトークンに高い重みを割り当てます。一方、「CSRF」「トークン検証」「SameSite Cookie」といったセキュリティ関連のトークンはプロンプト内に存在しないため、attentionがそれらの概念を自発的に想起する動機がありません。
これはLLMの本質的な限界です。LLMは「プロンプトに含まれる情報から最も確率の高い続きを生成する」機械であり、「プロンプトに含まれていないが本来必要な情報を補完する」のは苦手です。SQLインジェクション対策が比較的うまくいくのは、「SQLクエリを構築する」というコンテキスト自体がattentionを通じて「パラメータ化」という安全パターンを想起させやすいためです。しかしCSRFのように、フォーム実装のコンテキストとセキュリティ対策の間に意味的な距離がある場合、attentionはその橋渡しを自然には行いません。
コンテキストウィンドウの制約 ── セキュリティは複数ファイルにまたがる
セキュリティの問題は本質的にシステム横断的です。CSRFを例にとると、以下のコンポーネントが連携する必要があります。
- ミドルウェア: トークン生成と検証ロジック
- テンプレート/フロントエンド: フォームへのトークン埋め込み
- セッション管理: トークンとセッションの紐付け
- ルーティング: 保護対象エンドポイントの指定
AIエージェントが1つのファイルを編集しているとき、他のファイルとの整合性を完全に把握し続けるのは困難です。現在のコーディングエージェントは、タスク実行中にコンテキストウィンドウを効率的に使うために「今編集しているファイルに関連する情報」を優先的に保持します。その結果、セキュリティミドルウェアの設定ファイルやセッション管理のコードは、フォーム実装中にはコンテキストから押し出されやすくなります。プロジェクトが大規模になるほど、この問題は深刻化します。
OWASP Top 10への認識のムラ
AIツールの脆弱性検出能力には、カテゴリによって大きな差があります。Tenzaiの調査によれば、SQLインジェクション対策(パラメータ化クエリの使用)についてはほぼ全てのツールが適切に処理できていました。これは訓練データに「SQLインジェクションは危険、パラメータ化クエリを使え」というパターンが豊富に存在し、かつ「SQLクエリ構築」というコンテキストがattentionを通じて安全パターンを直接想起させるためです。
一方、CSRF、セキュリティヘッダ、認可チェックのような「明示的に要求されない限り実装されない」防御策は、ほぼ確実に欠落します。これらは機能実装のコンテキストとの意味的距離が遠く、プロンプトに明示されない限りattentionが到達しない領域です。AIは「聞かれたことに答える」のであって、「聞かれていないが必要なこと」を自発的に追加する動機を持っていません。
AIが得意なセキュリティ検査と苦手な検査
| カテゴリ | 得意/苦手 | 具体例 | 理由 |
|---|---|---|---|
| SQLインジェクション | 得意 | パラメータ化クエリの自動使用 | 訓練データに明確なパターンが豊富 |
| XSS(反射型) | 得意 | 出力エスケープの適用 | テンプレートエンジンのデフォルト動作と一致 |
| ハードコードされた秘密情報 | 得意 | APIキーの環境変数化を提案 | 「.envを使え」が定番パターン |
| 既知CVEのあるライブラリ | 得意 | 脆弱なバージョンの警告 | 学習データに修正履歴が含まれる |
| CSRF | 苦手 | トークン生成・検証の欠落 | 複数レイヤーの連携が必要 |
| 認可ロジックの不備 | 苦手 | IDOR(他ユーザーのリソースへのアクセス) | ビジネスロジックの理解が必要 |
| セキュリティヘッダ | 苦手 | CSP、HSTS等の設定漏れ | 機能要件に含まれない防御策 |
| 認証フローの設計ミス | 苦手 | セッション固定攻撃、トークン失効の不備 | 状態遷移の全体像の把握が必要 |
| SSRF | 苦手 | 内部URL呼び出しの未制限 | URL処理のセキュリティ境界の理解が必要 |
| ビジネスロジックの脆弱性 | 苦手 | 割引の重複適用、残高チェックの不備 | ドメイン知識とセキュリティの交差点 |
傾向を一言でまとめると、AIは「単一ファイル内で完結し、明確なパターンがある脆弱性」に強く、「複数コンポーネントにまたがり、コンテキスト依存の脆弱性」に弱いです。
得意な例: SQLインジェクションの検知と修正
AIに「ユーザー検索機能を作って」と指示すると、ほぼ確実にパラメータ化クエリで実装します。
# AIが生成するコード(Python / FastAPI + asyncpg)
@app.get("/users/search")
async def search_users(name: str):
# パラメータ化クエリを自動的に使用 ── SQLインジェクションは発生しない
rows = await db.fetch(
"SELECT id, name, email FROM users WHERE name ILIKE $1",
f"%{name}%"
)
return [{"id": r["id"], "name": r["name"], "email": r["email"]} for r in rows]
仮に文字列結合でSQLを構築するコードをレビューさせた場合も、高い確率で脆弱性を指摘します。
# レビュー対象として渡した脆弱なコード
query = f"SELECT * FROM users WHERE name LIKE '%{name}%'"
cursor.execute(query) # AIは「SQLインジェクションの脆弱性があります」と指摘できる
これが成功する理由は、「SQLクエリ構築」というコンテキスト自体が、訓練データにおいて「パラメータ化せよ」という安全パターンと強く結びついているためです。
苦手な例: ビジネスロジック脆弱性の見落とし
一方、以下のようなECサイトの割引適用コードをAIに書かせた場合、ビジネスロジックの脆弱性は見逃されます。
# AIが生成した割引適用コード(脆弱)
@app.post("/cart/apply-coupon")
async def apply_coupon(cart_id: int, coupon_code: str, user=Depends(get_current_user)):
coupon = await db.fetchrow(
"SELECT * FROM coupons WHERE code = $1 AND expires_at > NOW()", coupon_code
)
if not coupon:
raise HTTPException(status_code=400, detail="無効なクーポンです")
cart = await db.fetchrow("SELECT * FROM carts WHERE id = $1", cart_id)
# 割引を適用
new_total = cart["total"] - (cart["total"] * coupon["discount_rate"])
await db.execute(
"UPDATE carts SET total = $1 WHERE id = $2", new_total, cart_id
)
return {"new_total": new_total}
このコードには少なくとも3つのビジネスロジック脆弱性があります。
- 同じクーポンを何度も適用できる(重複適用チェックがない)
- 複数の異なるクーポンを重ねがけできる(割引の上限チェックがない)
- 割引後の金額がマイナスになりうる(下限チェックがない)
攻撃者は同じクーポンを10回適用するか、複数のクーポンを組み合わせることで、商品を無料または負の金額で購入できます。正しい実装は以下のようになります。
# 修正後のセキュアなコード
@app.post("/cart/apply-coupon")
async def apply_coupon(cart_id: int, coupon_code: str, user=Depends(get_current_user)):
coupon = await db.fetchrow(
"SELECT * FROM coupons WHERE code = $1 AND expires_at > NOW()", coupon_code
)
if not coupon:
raise HTTPException(status_code=400, detail="無効なクーポンです")
# 重複適用チェック
already_applied = await db.fetchrow(
"SELECT 1 FROM cart_coupons WHERE cart_id = $1 AND coupon_id = $2",
cart_id, coupon["id"]
)
if already_applied:
raise HTTPException(status_code=400, detail="このクーポンは適用済みです")
# クーポン併用上限チェック
applied_count = await db.fetchval(
"SELECT COUNT(*) FROM cart_coupons WHERE cart_id = $1", cart_id
)
if applied_count >= 1: # ビジネスルール: クーポンは1枚まで
raise HTTPException(status_code=400, detail="クーポンの併用はできません")
cart = await db.fetchrow("SELECT * FROM carts WHERE id = $1", cart_id)
new_total = max(
cart["total"] - (cart["total"] * coupon["discount_rate"]),
0 # 下限を0に制約
)
# トランザクション内でカートと適用履歴を同時更新
async with db.transaction():
await db.execute("UPDATE carts SET total = $1 WHERE id = $2", new_total, cart_id)
await db.execute(
"INSERT INTO cart_coupons (cart_id, coupon_id) VALUES ($1, $2)",
cart_id, coupon["id"]
)
return {"new_total": new_total}
AIがこの脆弱性を見逃す理由は明確です。「クーポンは1回しか使えない」「割引後の金額は0未満にならない」というのはビジネスルールであり、コードのパターンとして訓練データに存在しません。AIにとっては、割引を正しく計算すること自体が機能要件の達成であり、その機能が悪用されるシナリオを想定する動機がないのです。
Anthropicの0-Dayハンティング ── 500件超の発見とその限界
皮肉なことに、AIツールが生成するコードのセキュリティが問題になる一方で、AI自体は脆弱性の「発見」には力を発揮しています。
2026年2月、Anthropicはred.anthropic.comで、Claude Opus 4.6がオープンソースソフトウェアの500件を超える高深刻度のゼロデイ脆弱性を発見したと発表しました。対象は、何年にもわたってファザーが稼働し、数百万時間のCPU時間が投入されてきた、最もテストされたコードベースです。それでもClaudeは、数十年にわたって未検出だった脆弱性を発見しました。
ただし、ここには重要な非対称性があります。
- 「既存コードの脆弱性を見つける」= 既にある大量のコードを分析対象にできる
- 「新しいコードを安全に書く」= まだ存在しないコードの防御策を先回りで実装する必要がある
前者は「パターンマッチング」の延長で対応できますが、後者は「このアプリケーションにはどんな攻撃が可能か」を事前に構想する能力、つまり脅威モデリングの能力が求められます。現時点のAIコーディングツールは、指示されない限り脅威モデリングを自発的に行いません。
参考: Anthropic 0-Days / Claude AI finds 500 high-severity software vulnerabilities
人間 + AIのセキュリティレビューワークフロー
「AIに任せるか、人間がやるか」ではなく、強みを組み合わせた4段階のワークフローをCI/CDパイプラインとして設計します。
パイプライン全体像
[開発者のローカル環境]
|
| Phase 1: AIによるコード生成
| Phase 2: AIによるセキュリティスキャン(pre-commit hook)
|
v
[git push / Pull Request作成]
|
v
[CI Pipeline - GitHub Actions]
|
+-- Phase 3: 静的解析ゲート(自動)
| |-- Semgrep(カスタムルール + OWASP Top 10ルールセット)
| |-- npm audit / pip-audit(依存関係の脆弱性)
| |-- TruffleHog(シークレット検出)
| +-- 結果: SARIF形式でGitHub Security tabに統合
|
+-- Phase 4: 人間によるセキュリティレビュー(PRレビュー)
| |-- 認可ロジック・ビジネスロジックの妥当性
| |-- 認証フロー全体の整合性
| +-- データフローの追跡
|
v
[マージ → ステージング環境デプロイ]
|
+-- DAST(動的テスト / OWASP ZAP Baseline Scan)
|
v
[本番デプロイ]
Phase 1: AIによるコード生成
機能要件の実装に集中させます。この段階ではセキュリティより「正しく動くコード」を優先します。
Phase 2: AIによるセキュリティスキャン(pre-commit)
生成コードに対して、セキュリティ特化のプロンプトで再度AIにレビューさせます。これをpre-commitフックまたはPR作成時に実行することで、開発者がセキュリティレビューを忘れることを防ぎます。
以下のコードをOWASP Top 10 2025の観点でセキュリティレビューしてください。
特に以下を重点的に確認してください:
- CSRF対策の有無(状態変更を伴うPOST/PUT/DELETEエンドポイント)
- 認可チェック(リソースアクセス時のオーナー検証)
- セキュリティヘッダの設定状況
- セッション管理の安全性
- SSRF(外部URLを受け取る機能がある場合)
- レート制限の有無(認証エンドポイント)
「まず実装、次にセキュリティ」とステップ分離することで、AIのattentionをセキュリティに集中させられます。Phase 1で機能実装に使われていた注意リソースが、Phase 2ではセキュリティ検証に全振りされるため、見落としが減ります。
Phase 3: 静的解析ゲート(CI/CD)
GitHub Actionsでの実装例を示します。
# .github/workflows/security-gate.yml
name: Security Gate
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: |
semgrep scan \
--config "p/owasp-top-ten" \
--config "p/javascript" \
--config ".semgrep/" \
--sarif --output semgrep-results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep-results.sarif
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
# Python の場合: pip-audit --strict --desc
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
Semgrepの.semgrep/ディレクトリにはプロジェクト固有のカスタムルールを配置します。例えば、CSRFトークン検証なしのPOSTハンドラを検出するルールを書けます。
# .semgrep/csrf-check.yml
rules:
- id: missing-csrf-protection
patterns:
- pattern: app.post($PATH, ..., async (req, res) => { ... })
- pattern-not-inside: |
app.use(csrfProtection)
...
app.post(...)
message: "POSTエンドポイントにCSRF保護が適用されていません"
severity: ERROR
languages: [javascript, typescript]
全てのセキュリティジョブがパスしない限りPRのマージをブロックするブランチ保護ルールを設定します。これにより、AIと人間の両方が見逃した脆弱性を機械的に捕捉できます。
Phase 4: 人間によるセキュリティレビュー(PRレビュー)
静的解析では検出できない以下の項目を人間がレビューします。
- 認可ロジックの妥当性(「管理者だけがアクセスできるべきか」はビジネス判断)
- 認証フロー全体の整合性(ログイン → セッション発行 → 2FA → ログアウト → セッション無効化の状態遷移)
- データフローの追跡(ユーザー入力がどのコンポーネントを通過し、最終的にどこに保存・表示されるか)
- ビジネスロジックの悪用シナリオ(割引の重複適用、レースコンディションによる残高不整合など)
PRテンプレートにセキュリティチェックリストを含めることで、レビュアーの見落としを減らせます。
<!-- .github/pull_request_template.md に追加 -->
## セキュリティチェック
- [ ] 状態変更エンドポイントにCSRF対策があるか
- [ ] リソースアクセス時にオーナーシップを検証しているか
- [ ] ユーザー入力のバリデーションはサーバー側で行っているか
- [ ] 新しい外部依存にCVEがないか(`npm audit` / `pip-audit`)
- [ ] 認証・認可の変更がある場合、フロー全体を確認したか
CLAUDE.mdにセキュリティチェックを組み込む
Claude Codeを使う場合、プロジェクトルートのCLAUDE.mdにセキュリティルールを記述することで、生成コードの品質を底上げできます。CLAUDE.mdはセッション開始時に自動読み込みされるため、毎回プロンプトで指示する手間が省けます。
以下は、Tenzaiレポートで検出された69件の脆弱性カテゴリを網羅し、実際のプロジェクトにコピー&ペーストで導入できるレベルのルール全文です。
## セキュリティルール
このルールはClaude Codeによるコード生成時に常に適用される。
違反するコードを生成してはならない。判断に迷う場合はユーザーに確認すること。
### 1. 入力バリデーション
- ユーザー入力は全てサーバー側でバリデーションすること。フロントエンドのバリデーションはUX目的のみであり、セキュリティ境界としてカウントしない
- バリデーションはホワイトリスト方式(許可する値を定義)を優先すること。ブラックリスト方式(禁止する値を定義)は回避策が存在するため非推奨
- ファイルアップロードは拡張子だけでなくMIMEタイプとマジックバイトを検証すること
- 入力値のサイズ上限を設定すること(ReDoS、メモリ枯渇の防止)
### 2. SQLインジェクション防止
- SQLクエリは必ずパラメータ化クエリ(プレースホルダ)を使用すること
- 文字列結合、テンプレートリテラル、f-stringによるSQL構築は禁止
- ORMを使用する場合もraw queryには同じルールを適用すること
- `ORDER BY`やテーブル名など、パラメータ化できない動的SQLはホワイトリストで制約すること
### 3. CSRF対策
- 状態変更を伴う全てのエンドポイント(POST/PUT/PATCH/DELETE)にCSRFトークン検証を実装すること
- CSRFトークンはセッションに紐付け、リクエストごとに検証すること
- SameSite Cookie属性をStrictまたはLaxに設定すること
- APIがCookie認証を使用する場合、CORSのAccess-Control-Allow-Originにワイルドカード(*)を設定しないこと
- フレームワーク別の実装:
- Express.js: csrf-csrfまたはlusca を使用
- Django: CsrfViewMiddlewareを無効化しないこと
- Next.js: Server ActionsはCSRFトークンを自動検証するが、API RoutesはOriginヘッダ検証を追加すること
- Rails: protect_from_forgeryをスキップしないこと
### 4. 認証
- パスワードはbcrypt(cost factor 12以上)またはargon2idでハッシュ化すること
- SHA-256、MD5、SHA-1によるパスワードハッシュは禁止
- セッショントークンは暗号論的に安全な乱数生成器(crypto.randomBytes、secrets.token_urlsafe等)で生成すること。最低32バイト
- ログイン失敗時のレスポンスから「ユーザーが存在するか」を推測できないようにすること(ユーザー列挙防止)
- ログイン試行にレート制限を設けること(ブルートフォース防止)
- 2FA設定変更、パスワード変更、メールアドレス変更には再認証を要求すること
- JWTを使用する場合:
- algorithm: "none"を拒否すること
- 秘密鍵は環境変数で管理すること(ハードコード禁止)
- 有効期限(exp)を設定し、リフレッシュトークンのローテーションを実装すること
### 5. 認可
- リソースアクセス時は必ずオーナーシップまたはロールの検証を行うこと(IDOR防止)
- `/users/:id/profile`のようなエンドポイントでは、URLパラメータのidとセッションのユーザーIDが一致するか、または適切な権限があるかを検証すること
- 管理者機能にはロールベースのアクセス制御を実装すること
- 認可チェックはミドルウェアまたはデコレータで一元化し、個別のハンドラで忘れるリスクを排除すること
### 6. セキュリティヘッダ
新しいWebアプリケーションを作成する際は、以下のヘッダを初期設定に含めること。
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Express.jsの場合はhelmetミドルウェアで一括設定すること。
### 7. SSRF防止
- 外部URLを受け取る機能(URLプレビュー、Webhook、画像取得等)では、リクエスト先をホワイトリストで制約すること
- プライベートIPアドレス範囲(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.169.254)へのリクエストをブロックすること
- DNSリバインディング対策として、名前解決後のIPアドレスも検証すること
- リダイレクトを追跡する場合は、リダイレクト先のURLにも同じバリデーションを適用すること
### 8. エラーハンドリング
- 本番環境ではスタックトレース、内部パス、SQL文、設定情報をAPIレスポンスに含めないこと
- エラーレスポンスは一般的なメッセージ("処理中にエラーが発生しました")に留め、詳細はサーバーログにのみ記録すること
- 404と403を区別して返すことで情報が漏洩しないか確認すること(リソースの存在を隠すべき場合は一律404を返す)
### 9. 依存関係
- APIキー、データベース接続文字列、シークレットはコードにハードコードせず環境変数で管理すること
- .envファイルは.gitignoreに含めること
- 新しいパッケージを追加する際はCVEがないことを確認すること
- lockファイル(package-lock.json、poetry.lock等)をコミットに含めること
### 10. セッション管理
- ログイン成功時にセッションIDを再生成すること(セッション固定攻撃防止)
- ログアウト時にサーバー側でセッションを無効化すること(クライアント側のCookie削除だけでは不十分)
- セッションCookieにはHttpOnly、Secure、SameSite属性を設定すること
- セッションの有効期限を設定し、長期間のアイドルセッションを自動的に無効化すること
なぜこのルールが効くのか
このルールが効く理由は、AIの「聞かれたことに答える」特性を逆手に取っている点です。元々の問題は「AIがCSRFの必要性を自発的に判断しない」ことでしたが、CLAUDE.mdで「状態変更を伴う全てのエンドポイントにCSRFトークン検証を実装すること」と具体的に指示すれば、フォーム実装時にCSRFトークンを含めるべきだというコンテキストがattentionの対象になります。
ルールを記述する際のポイントは以下の3点です。
- 抽象的な指針ではなく具体的な行動を記述する。「セキュリティに気をつけること」ではなく「POST/PUT/PATCH/DELETEにはCSRFトークン検証を実装すること」のように、AIが機械的に従えるレベルまで具体化する
- 禁止事項を明示する。「パラメータ化クエリを使え」だけでなく「文字列結合でのSQL構築は禁止」と書くことで、AIが「動くが危険なパターン」を選択する確率を下げる
- フレームワーク固有の実装方法を含める。「CSRF対策をせよ」だけでは不十分で、「Express.jsの場合はcsrf-csrfを使用」のように具体的なライブラリ名まで指定する
ただし、CLAUDE.mdはあくまで「プロンプト」であり強制力はありません。AIがルールを無視する可能性はゼロではないため、CI/CDパイプラインでの静的解析による機械的な検証は依然として必要です。CLAUDE.mdは「AIの生成品質を上げる」施策であり、「脆弱性を確実に防ぐ」施策ではないことを理解しておく必要があります。
より包括的なルールセットとしては、OpenSSFのSecurity-Focused Guide for AI Code Assistant Instructionsや、OWASP ASVS 5.0準拠のclaude-code-owaspスキルが参考になります。
結論 ── AIを「賢いジュニア開発者」として扱う
2026年現在のAIコーディングツールは、セキュリティに関しては「賢いが経験の浅いジュニア開発者」と同程度の能力です。コードは書けるが、攻撃者の視点を持っていない。機能要件には忠実だが、非機能要件(特にセキュリティ)は明示的に指示しない限り考慮しない。
一方で、Anthropicの0-Dayハンティングプログラムが示したように、「脆弱性を見つける」というタスクに特化させれば、人間の研究者を補完する強力なツールになります。問題は「コード生成」と「セキュリティ検証」を1つのプロセスで同時に達成しようとすることです。
実務での推奨アプローチは以下の通りです。
- CLAUDE.mdや指示ファイルにセキュリティルールを明記し、AIの「デフォルト行動」を安全寄りに矯正する
- コード生成とセキュリティレビューをフェーズ分離し、それぞれにAIを使う
- 認可ロジック、認証フロー、ビジネスロジックのセキュリティは人間がレビューする
- CI/CDパイプラインに静的解析ツールを組み込み、最後の防衛線とする
AIコーディングツールの生産性向上は本物です。しかし、その出力を無検証で本番に投入するのは、ジュニア開発者のPRをコードレビューなしでマージするのと同じリスクを負うことになります。
参考リンク
- Tenzai - Bad Vibes: Comparing the Secure Coding Capabilities of Popular Coding Agents
- Anthropic 0-Days
- Vibe Coding Is a Security Catastrophe: 69 Vulnerabilities Found
- AI coding agents keep repeating decade-old security mistakes - Help Net Security
- OpenSSF Security-Focused Guide for AI Code Assistant Instructions
- claude-code-owasp (OWASP Security Skill)
- Vibe Coding 2026: 2,000 Vulnerabilities, 400 Exposed Secrets