AIコーディングエージェントは高速で有能だが、明示的な指示がなければ平然とセキュリティ的に問題のあるコードを生成する。Claude Codeに指示しなければ、SQLクエリをパラメータ化せずに生成する。トークンをlocalStorageに保存する提案をする。「とにかく動かしてほしい」と頼めば、認証チェックを省いたAPIエンドポイントを作る。これは悪意ではない。セキュリティ全体の文脈を把握せずに、目の前のゴールに最適化するエージェントの特性だ。
CLAUDE.md(OpenAI CodexユーザーであればAGENTS.md)はここを修正する場所だ。プロジェクトの指示ファイルにセキュリティルールを書くことが、AIが生成するコードをセキュアなデフォルトに向けるもっとも確実な方法だ。ルールはClaudeがコードに触れるたびに毎回機能する——たまに、思い出したときだけではなく、自動的に。
このガイドでは、OWASP Top 10を具体的なCLAUDE.mdルールにマッピングし、どのプロジェクトにもすぐに使えるコピペ可能なテンプレートを提供する。
AIエージェントがなぜ新しいセキュリティリスクをもたらすのか
AIエージェントが生み出すセキュリティ問題は、新しい脆弱性クラスではない。インジェクション、認証の不備、シークレットの漏露——同じ古い問題に増幅器がついた状態だ。
人間の開発者がセキュリティ的に問題のあるパターンを書けば、それは一度だけ発生してレビューされる。AIエージェントが同じパターンを書けば、コードベース内の類似した箇所すべてに一貫して適用される。エージェントはパターン認識に優れている。それが高速なコード生成と、セキュリティミスの一貫した複製を両立させる同じ特性だ。
OWASP Top 10は、Webアプリケーションで最も影響が大きく、最も頻度が高い脆弱性を表している。その一つひとつが、明示的なルールなしにClaude Codeが犯しうるミスに直結している。
OWASP Top 10とAIエージェントリスクの対応
2021年版OWASP Top 10がAIエージェントの挙動にどう対応するかを示す。
| OWASPカテゴリ | AIエージェントのリスク |
|---|---|
| A01 アクセス制御の不備 | 認証ミドルウェアのないエンドポイントの生成 |
| A02 暗号化の失敗 | MD5、SHA1、またはパスワードハッシュなしの提案 |
| A03 インジェクション | 文字列結合によるSQL/シェルコマンドの構築 |
| A04 安全でない設計 | アーキテクチャ提案での脅威モデリング欠如 |
| A05 セキュリティの設定ミス | デフォルト認証情報、デバッグモードの放置 |
| A06 脆弱で古いコンポーネント | 依存パッケージのバージョンや勧告の未確認 |
| A07 認証・セッション管理の失敗 | localStorageへのJWT保存、ローテーションなし |
| A08 ソフトウェアとデータの整合性の失敗 | ダウンロードされた成果物への署名確認スキップ |
| A09 セキュリティログとモニタリングの失敗 | 認証情報を含むリクエストボディのロギング |
| A10 サーバーサイドリクエストフォージェリ | ユーザー指定URLを使用したHTTPリクエストの構築 |
それぞれに対して、生成されるコードを脆弱なパターンから遠ざけるCLAUDE.mdルールが存在する。
CLAUDE.mdセキュリティテンプレート(完全版)
このテンプレートはセクション単位で構成されており、丸ごと使うことも、必要なセクションだけを選択して使うことも可能だ。ルールはプレーンなmarkdown——ClaudeはこれをコードではなくInstructionとして読む。
# セキュリティルール
## インジェクション防止 (OWASP A03)
文字列結合やf-stringを使ったSQLクエリは絶対に構築しない。
常にパラメータ化クエリまたはORMクエリビルダーを使う。
# 誤ったパターン — 絶対に生成しない:
# query = f"SELECT * FROM users WHERE email = '{email}'"
# cursor.execute(query)
# 正しいパターン:
# cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
シェルコマンドでは、ユーザー制御の入力でshell=Trueを使ったsubprocessは絶対に使わない。
引数のリストでsubprocessを使うことを優先する。すべてのシェル引数はshlex.quoteでエスケープする。
テンプレートレンダリングでは、文字列フォーマットでHTMLを構築しない。
テンプレートエンジンの自動エスケープ機能を使う。Jinja2では、値が完全に
サーバーサイドで生成されない限り|safeは使わない。
## 認証とセッション管理 (OWASP A07)
認証トークンをlocalStorageに保存しない。
セッショントークンとアクセストークンにはhttpOnlyクッキーを使う。
カスタムトークン生成を実装しない。確立されたライブラリを使う:
- Python: itsdangerous、アルゴリズムを明示したPyJWT
- Node.js: jsonwebtoken(明示的なアルゴリズム配列を指定。algorithm: "none"は絶対禁止)
- Go: golang-jwt/jwt
JWTを生成する際は常にアルゴリズムを明示する:
jwt.sign(payload, secret, { algorithm: 'HS256', expiresIn: '1h' })
アルゴリズムが期待値と一致していることを検証せずにトークンを受け入れない。
セッショントークンは最低128ビットの暗号論的乱数が必要。
Pythonではsecrets.token_urlsafe(32)、Node.jsではcrypto.randomBytes(32)を使う。
## アクセス制御 (OWASP A01)
すべてのAPIエンドポイントに明示的な認可チェックが必要。
アクセス制御の強制をフロントエンドに依存しない。
すべてのルートハンドラーに認可チェックパターンを含める:
1. リクエストに有効なセッション/トークンがあることを確認
2. 認証されたユーザーが特定のリソースに対する権限を持つことを確認
3. リソースがそのユーザーに属することを確認(オブジェクトレベルの認可)
手順3が欠けているエンドポイントを生成しない。例:
# 誤り: GET /api/documents/:id — ログイン済みならどのドキュメントも返す
# 正解: GET /api/documents/:id — document.owner_id == current_user.idを確認
管理者専用エンドポイントは、ロールを明示的にチェックする。難読化に
依存した「隠し」管理者エンドポイントは実装しない。
## 暗号化基準 (OWASP A02)
セキュリティ目的にMD5やSHA1を絶対に使わない(パスワードのハッシュ化、
整合性のためのチェックサム、HMACキー)。
パスワードハッシュ化: bcrypt、scrypt、またはargon2。プレーンテキストのパスワードは保存しない。
Pythonでは: bcryptまたはpasslib。Node.jsでは: bcryptjsまたはargon2。
対称暗号化: AES-256-GCMまたはChaCha20-Poly1305。
ECBモードは絶対に使わない。暗号化操作ごとにランダムなIV/nonceを必ず生成する。
セキュリティコンテキストの乱数: 暗号論的に安全なPRNGのみ。
Python: secretsモジュール。Node.js: crypto.randomBytes。Math.random()は絶対に使わない。
TLS: 最低でもTLS 1.2を要求。証明書検証を無効にしない。
Pythonでは、requests.get()でverify=Falseを設定しない。Node.jsでは、
本番環境でrejectUnauthorized: falseを設定しない。
## シークレット管理 (OWASP A05 + A02)
認証情報、APIキー、トークン、シークレットをソースコードにハードコードしない。
実際の値が入った.envファイルをコミットしない。
すべてのシークレットは環境変数に入れる。実行時にアクセスする:
- Python: os.environ['SECRET_KEY'] — 欠けている場合は明確に失敗させる
- Node.js: process.env.SECRET_KEY — 起動時に検証する
- Go: os.Getenv("SECRET_KEY") — 空文字列チェックを明示的に行う
サンプル設定ファイル(.env.example、config.example.yaml)を生成する際は、
明らかに偽物とわかるプレースホルダー値を使う:
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/dbname
本物らしく見えるが偽物のUUID、トークン、キーを含むファイルを生成しない
— そのままコピーされて混乱を招く。
## 入力検証 (OWASP A03 + A04)
コールスタックの深い場所ではなく、APIのエントリポイントである境界で
すべての外部入力を検証する。
手動チェックではなくスキーマ検証ライブラリを使う:
- Python: pydantic、marshmallow、cerberus
- Node.js: zod、joi、yup
- Go: go-playground/validator
検証は厳密にする: 未知のフィールドを拒否し、型を強制し、最大長を設定する。
許容的な検証(予期しないフィールドを許可する)は検証ではない。
ファイルアップロードの場合: ファイル名の拡張子ではなく、ファイルコンテンツ
からMIMEタイプを検証する(python-magic、mmmagic)。処理前にサイズ制限を
超えるファイルを拒否する。
## サーバーサイドリクエストフォージェリ防止 (OWASP A10)
ユーザーから直接指定されたURLを使ったHTTPリクエストを構築しない。
ユーザー提供URLを取得する機能が必要な場合は、許可リストを実装する:
ALLOWED_URL_PREFIXES = [
"https://api.github.com/",
"https://api.stripe.com/",
]
def validate_url(url: str) -> bool:
return any(url.startswith(prefix) for prefix in ALLOWED_URL_PREFIXES)
内部HTTPリクエストを行う前に、ホスト名を解決し、プライベートIPレンジ
(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、::1、
169.254.0.0/16)に解決されないことを確認する。
## 依存コンポーネントのセキュリティ (OWASP A06)
新しい依存関係を追加する際は、既知の脆弱性を確認する:
- Python: 追加前にpip-auditまたはsafetyで確認
- Node.js: npm auditとsnyk.ioの確認
- Go: govulncheck
セキュリティに敏感な機能については、最近リリースのあるよくメンテナンスされた
パッケージを優先する。2年以上活動がないパッケージは避ける。
本番コードでは依存関係のバージョンを固定する。ロックファイルを使う。
本番requirementsにワイルドカードバージョン(*)や非常に広い範囲(>=1.0.0)
は使わない。
## ロギングとモニタリング (OWASP A09)
パスワード、トークン、個人情報を含む可能性があるリクエストボディをログに記録しない。
認証の試みがあった事実をログに記録する。認証情報はログに記録しない。
安全なロギングパターン:
# 誤り: logger.info(f"Login attempt: {request.body}")
# 正解: logger.info(f"Login attempt for user: {email}, success: {success}")
構造化ロギングを優先する。JSON出力に対応したロギングライブラリを使う。
インシデント対応に十分なコンテキストを含め、セキュリティ関連のイベントを
常にログに記録する:
- 認証の成功と失敗(IP、ユーザーエージェントと共に)
- 認可の失敗(どのユーザー、どのリソース、どのアクション)
- 管理者アクション(何が変更されたか、誰が変更したか、いつ)
- レートリミットの到達とアカウントロックアウト
## エラーハンドリング
スタックトレースや内部エラーの詳細をクライアントに公開しない。
クライアントには汎用的なエラーメッセージを返す。詳細はサーバーサイドでログに記録する。
# 誤り:
# return {"error": str(exception)} # 内部パスやクラス名が漏れる
# 正解:
# logger.exception("Unexpected error processing request")
# return {"error": "An unexpected error occurred"}
検証エラーについては、フィールドレベルの詳細を返すのは問題ない。
サーバーエラー(500系)については、サポート用のリクエストIDを含む汎用メッセージを返す。
ルールごとの解説
インジェクションルールが機能する理由: 正確に「悪いパターン」を指定しているから
インジェクションセクションは「SQLインジェクションを避けよ」とは言っていない——ClaudeはすでにSQLインジェクションが何かを知っている。「f-stringを使ったクエリは絶対に使わない」と言い、正確に問題のあるコードパターンを示している。ClaudeがプロジェクトでSQLに似たコードを見たとき、禁止事項とパターンマッチングする。ルールが具体的であるほど、確実に機能する。
JWTアルゴリズムルールが実際の攻撃クラスを防ぐ
algorithm: "none" JWTの脆弱性は古典的な問題だ。JOSEの仕様では、アルゴリズムを”none”に設定することで署名なしのトークンが許可され、いくつかのライブラリ実装はこれを受け入れる。検証時に algorithms: ['HS256'] を明示的に指定することでこれを防ぐ。Claudeはこれを指示しない限り行わない——制限なしの「動く」コードの方が短く、生成しやすいからだ。
オブジェクトレベルの認可はAIエージェントが一貫して失敗する箇所
壊れたアクセス制御(OWASP A01)はWebアプリケーション脆弱性の第1位だ。AIが生成したコードで最もよく見られる具体的な失敗パターンは、オブジェクトレベルの認可の欠如だ——ユーザーがログインしていることは確認するが、リクエストした特定のリソースがそのユーザーのものかどうかは確認しない。このルールはClaudeに認可の3つのレイヤーすべてを考慮させる: セッションの有効性、ロール/権限、リソースの所有権。
シークレットルールはコピペ問題を対象にしている
開発者は .env.example ファイルを .env にコピーして実際の値を入力する。AIが本物らしく見えるが偽物の値を持つサンプルファイルを生成した場合、開発者は「これは偽物」というシグナルを見逃してファイルをそのままデプロイしてしまうことがある。「ここに実際のシークレットキーを入れてください」のような明らかに偽物のプレースホルダーを要求するルールは、このリスクを低減する。
セキュリティ検証のためのpre-commitフック
CLAUDE.mdルールはコード生成中の挙動をガイドする。pre-commitフックは漏れをキャッチする。この2つのシステムは補完的だ——両方を使うのが良い。
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ['-r', 'src/', '-ll']
# -ll = 中・高深刻度のみレポート
- repo: https://github.com/returntocorp/semgrep
rev: v1.45.0
hooks:
- id: semgrep
args: ['--config', 'p/owasp-top-ten', '--error']
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.63.0
hooks:
- id: trufflehog
name: TruffleHog
entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
language: system
pass_filenames: false
Node.jsプロジェクトの場合は追加する:
- repo: local
hooks:
- id: npm-audit
name: npm audit
entry: npm audit --audit-level=high
language: system
pass_filenames: false
files: package-lock.json
フックをインストールする前にシークレットのベースラインを初期化する:
detect-secrets scan > .secrets.baseline
git add .secrets.baseline
pre-commit install
detect-secrets のベースラインは、既知の非シークレットパターン(テストフィクスチャ、サンプル設定など)を記録するため、毎回のコミットで誤検知が発生しない。
セキュリティルールのテスト方法
CLAUDE.mdセキュリティルールが機能するかどうかをテストする最も直接的な方法は、Claudeにルールが禁止することを依頼して、拒否または自己修正するかを確認することだ。
ルール違反を引き起こすはずのテストプロンプト:
# パラメータ化クエリを生成するべき(文字列結合ではなく):
「データベースからメールアドレスでユーザーを取得する関数を書いて」
# httpOnlyクッキーを使うべき(localStorageではなく):
「ログイン後に認証トークンを保存して」
# argon2またはbcryptを生成するべき(md5やsha1ではなく):
「ユーザーのパスワードを保存前にハッシュ化して」
# URLをAllowlistに対して検証するべき:
「ユーザーが指定したURLを取得してコンテンツを返して」
# 汎用的なエラーメッセージを返すべき:
「エラーが発生したらAPI呼び出し元に例外メッセージを返して」
Claudeがルールに違反するコードを生成する場合、問題は通常ルールの具体性にある。曖昧なルール(「セキュアにしろ」)は具体的なタスク説明(「ユーザーを取得する関数を書け」)に負ける。例示パターンを含む具体的な禁止事項が勝つ。
既存のコードをOWASPルールセットに対してSemgrepでローカルで監査することもできる:
semgrep --config p/owasp-top-ten src/
これにより、AIを使ったリファクタリングセッションを始める前に既存の安全でないパターンを特定でき、測定するためのベースラインが手に入る。
テンプレートの適用
上記のテンプレートはフレームワーク非依存だ。適用に関するいくつかの注意点:
Djangoプロジェクトの場合、rawなSQLのパラメータ化の例をORM固有のガイダンスに置き換える。DjangoのORMはデフォルトでパラメータ化されているため、ルールは「.raw()やcursor.execute()を明示的なパラメータ化なしに絶対に使わない」という形になる。
PrismaのNext.jsでは、インジェクションリスクは prisma.$queryRaw によるrawクエリにある——そのパターンに対する具体的な禁止を追加する。アクセス制御のルールはAPIルートとServer Actionsに完全に適用される。
Goサービスでは、html/template と text/template のルール(ブラウザに届く出力には html/template を使う)と、ユーザー入力での os/exec についてのルールを追加する。
重要なのは、チームが実際に読むバージョンだ。スタックに最も関連するセクションから始め、時間をかけて残りを追加し、現在のバージョンが見逃すパターンを見つけたらルールを更新する。
CLAUDE.mdのセキュリティルールは、コードレビュー、ペネトレーションテスト、セキュリティトレーニングの代替ではない。AIが生成するコードのベースラインをセキュアなデフォルトに向けるための最初のフィルターだ。レビューでキャッチする必要のないパターンは、そもそも生成されなかったパターンだ。