きっかけ
⚠️ 本記事のコードはサンプルです。実際の認証情報・設定値は含まれていません。
ClaudeCodeNaviにはMCPツールがあり、Claude Codeから記事を直接投稿できます。
便利なのですが、あるときAIが書いた記事にセキュリティ上まずい内容が含まれていたことに気づきました。
具体的には「Mac Clawのエージェント間価格交渉APIを実装した」という記事を投稿した際、以下が含まれていました。
- 本番DBの実テーブル名・カラム名・インデックス構成(CREATE TABLE文がそのまま)
- Stripeの off_session / confirm フラグの詳細(3Dセキュア非通過フローを示す)
- 自動承認判定ロジックのコード(price と auto_approve_limit の比較式)
- min_price隠蔽ロジックの説明(実装バグと合わせて攻撃ヒントになる)
人間が書く記事なら「これは書いたらまずい」と気づけます。しかしAIがコードを読みながら自動生成した記事は、実装の詳細をそのまま書いてしまう傾向があります。
「気をつけよう」では解決しない。サーバー側で検出するしかない、と判断しました。
問題の整理:どの情報が危険か
機密情報の漏洩リスクは2段階に分けられます。
ブロックすべき(即座に投稿を止める)
| 内容 | リスク |
|------|-------|
| Stripe本番シークレットキー(sk_live_) | 即座に不正決済が可能 |
| Webhookシークレット(whsec_) | イベント偽装が可能 |
| SSH秘密鍵 | サーバーへの不正アクセス |
| .envファイルの内容(KEY=VALUE が3行以上) | 複数の機密情報が一括露出 |
| 汎用的な秘密鍵パターン(password = [長い文字列]) | 認証情報の露出 |
警告を出す(投稿は通すが知らせる)
| 内容 | リスク |
|------|-------|
| 本番サーバーのIPアドレス | ネットワーク構成の露出 |
| 内部ファイルパス(/var/www/...) | ディレクトリ構成の露出 |
| 実DBスキーマ(CREATE/ALTER TABLE + インデックス定義) | SQLi発生時のエクスプロイト容易化 |
| Stripeのoff_session決済フロー詳細 | フィッシング悪用リスク |
| 自動承認判定ロジックの詳細 | 攻撃シナリオの手引き |
技術記事である以上、コードを書くことは普通です。すべてのコードをブロックするのではなく、実際のキー・パス・スキーマが入っているときだけ検出する設計にしました。
実装:common/security-check.php
function checkArticleSecurity(string $title, string $body): array
{
// コードブロック内は除外してチェック(サンプルコードを誤検出しない)
$bodyStripped = preg_replace('/```[\s\S]*?```/', '', $body);
$bodyStripped = preg_replace('/`[^`]+`/', '', $bodyStripped);
$text = $title . "\n" . $bodyStripped;
$warnings = [];
$blocked = false;
$blockReason = null;
// ── BLOCK パターン ──────────────────────────────
// Stripe本番シークレットキー
if (preg_match('/sk_live_[A-Za-z0-9]{20,}/', $text)) {
$blocked = true;
$blockReason = 'Stripe本番シークレットキーが含まれています。';
}
// .envファイルの内容(KEY=VALUE形式が3行以上連続)
if (!$blocked) {
$envLines = preg_match_all('/^[A-Z_]{3,}=[^\n]{8,}$/m', $text, $matches);
if ($envLines >= 3) {
$blocked = true;
$blockReason = '.envファイルの内容と思われる記述が含まれています。';
}
}
// ── WARN パターン ───────────────────────────────
// 実DBスキーマ(CREATE/ALTER TABLE + インデックス定義がセット)
if (preg_match('/(?:CREATE TABLE|ALTER TABLE)\s+\w+/i', $text) &&
preg_match('/(?:INDEX|PRIMARY KEY|FOREIGN KEY)/i', $text)) {
$warnings[] = 'セキュリティ注意: 実際のDBスキーマが含まれています。本番テーブル名は概念的なサンプルに置き換えることを推奨します。';
}
return [
'blocked' => $blocked,
'block_reason' => $blockReason,
'warnings' => $warnings,
];
}
重要な設計判断:コードブロックは除外する
最初の実装では記事本文全体を検査していたため、説明用に書いたサンプルコード(-----BEGIN PRIVATE KEY----- のような例示)まで検出してブロックしてしまいました。コードブロック内は除外することで、技術記事として必要な説明コードは通しつつ、本文中に実際のキーが書かれた場合だけ検出できます。
API側への組み込み
createArticle() と updateArticle() の両方に追加しました。
// セキュリティチェック(バリデーション直後に実行)
$secResult = checkArticleSecurity($title, $body);
if ($secResult['blocked']) {
apiError($secResult['block_reason'], 400, 'SECURITY_BLOCK');
}
// 警告はseo_warningsに追記してレスポンスで返す
if (!empty($secResult['warnings'])) {
$seoWarnings = array_merge($seoWarnings, $secResult['warnings']);
}
MCPツール経由で投稿した場合、BLOCKなら400エラーが返りClaude Codeが修正を促します。WARNなら投稿は通りますが security_warnings としてレスポンスに含まれ、Claude Codeが「このような警告が出ています」と伝えてくれます。
Before / After
Before(今まで)
- MCPで記事を投稿 → そのまま公開
- 後から読み直して「あ、まずい情報が入ってた」と気づく
- 手動で記事を更新して修正
After(これから)
- MCPで記事を投稿 → サーバー側でセキュリティチェック
- BLOCKパターンにヒット → 400エラー、Claude Codeが修正を促す
- WARNパターンにヒット → 投稿は通るが警告をレスポンスに含める
- Claude Codeが警告を読んで「このような内容が含まれています、確認してください」と伝える
AIが書いた記事をAIが投稿する場合、人間のレビューが入らないケースがあります。サーバー側でチェックすることで、誰が投稿しても機密情報が漏れにくくなります。
まとめ
- AIが書いた技術記事は実装の詳細をそのまま書いてしまう傾向がある
- 「気をつける」ではなく、サーバー側チェックで仕組みとして防ぐ
- BLOCKとWARNを分けることで、技術記事として必要なコードは通しつつ本物のキー・スキーマだけを検出できる
- コードブロック内は除外することで誤検出を防ぐ
- MCPツール経由の自動投稿にも同様に機能する
ClaudeCodeNaviへの投稿はMCP経由でも、Webからでも同じチェックが走ります。安心して技術的な内容を書いてください。
https://claudecodenavi.jp