はじめに
2026年7月22日に、Anthropic 公式の Claude Security プラグイン がベータ版として公開されました。実際に使ってみた結果をまとめたいと思います。
Claude Security プラグインとは
Claude Security プラグインは、ローカルの Claude Code セッション内で、コードの脆弱性をマルチエージェントでスキャンする Anthropic 公式のプラグインです。複数のClaudeエージェントがチームとして動き、スキャンは大きく4つの段階で進みます。
公式の説明と実行結果から自分なりに理解すると、以下の様になっているようです。
- アーキテクチャのマッピング — スキャン対象リポジトリのコード構造をコンポーネントに分けて把握します。
- 脅威モデルの構築 — そのコードベースで何が攻撃対象になりうるかを整理します。
- 脆弱性のハント — コンポーネントごとにエージェントを割り当て、並列で脆弱性を探索します。
- 検出結果の検証 — 挙がった候補を、探索したエージェントとは別の独立したエージェントが1件ずつレビューし、通過したものだけでレポートを作成します。
導入
- 前提条件
- Claude Code v2.1.154 以降 + 有料プラン(スキャンが dynamic workflows を使うため)
- Python 3.9.6 以降が
python3として PATH が通っていること - 差分スキャンとパッチ生成には Git が必要
- インストール手順(公式マーケットプレイスから)
12/plugin install claude-security@claude-plugins-official/reload-plugins
できること
プラグインが追加するコマンドは
/claude-security
の1つだけです。このコマンドを実行するとメニューが開き、以下3つの中から選択して実行されます。最初にスキャン(全体 or 一部)して脆弱性の検出を行ったら、次にパッチファイルを生成してコードに適用するという流れです。
- Scan codebase(全体スキャン) — コードベース全体をスキャンします。
- Scan changes(差分スキャン) — ブランチやPRの差分、コミット指定など、変更部分だけをスキャンします。
- Suggest patches(パッチ生成) — 検出された問題の修正案を
.patchファイルとして提示します。自動適用はされず、人間が明示的に適用します。
また、
/claude-security scan my branch
や「scan commit abc1234」のように自然言語で直接指示することもできます。
実際に動かしてみる(検証)
意図的に脆弱なコードをテスト用Laravel/Reactリポジトリに仕込み、検出できるかを検証したいと思います。
脆弱性のあるコードを3箇所仕込み、それぞれ、「仕込んだコード」「検出結果」「生成されたパッチ」をまとめます。
因みに、このテスト用リポジトリには、検証対象ファイル数が200近くあったのですが、FableでMax 5x プランで全体スキャンした所、5時間リミットに20分くらいで到達してしまいました。。。
スキャン実行前にも大量のトークンを消費すると注意換気がされました。(その時はFableを使っていたので余計にトークン消費していたかもしれない)
検証時は仕込んだコードを含む一部ファイルのみの、一部スキャンとしています。
検証1: SQLインジェクション
注文検索のエンドポイントです。検索キーワードと並び順を、クエリパラメータからそのままSQL文に埋め込んでいます。
?keyword=nope' or '1'='1
を渡すと、条件に一致しないはずの注文まで全件返ってきます。
| 1 2 3 4 5 6 7 8 9 10 | $keyword = (string) $request->query('keyword', 'completed'); $sort = (string) $request->query('sort', 'created_at'); $direction = (string) $request->query('direction', 'desc'); $orders = DB::select( "select id, user_id, total_amount, status, created_at from orders where status = '{$keyword}' order by {$sort} {$direction}" ); |
検証2: XSS
フロント表示で、クエリパラメータの値を、エスケープせずにそのまま描画しています。サーバー側(Blade)とクライアント側(React)の両方にを用意しています。
{!! !!}
も
dangerouslySetInnerHTML
も、フレームワーク標準のエスケープを自分で無効化するものです。
<script>alert(1)</script>
などの悪意あるscriptがそのまま実行されます。
| 1 2 3 | // blade(サーバー) <h1>{{ $title }}</h1> <div class="note-body">{!! $body !!}</div> |
| 1 2 3 4 5 | // React(クライアント) <div className="mt-4 text-gray-700" dangerouslySetInnerHTML={{__html: body}} /> |
検証3: 認可不備
注文の請求情報を返すエンドポイントです。前提として、
orders
テーブルは
user_id
を持っており、注文には必ず持ち主がいます。ルート側は
auth
ミドルウェアを通しているので、ログインしていない人はアクセスできません。
| 1 2 3 4 | // routes/web.php Route::middleware('auth')->group(function () { Route::get('/orders/{orderId}/invoice', [InvoiceController::class, 'show']); }); |
問題のあるコントローラ側です。URLで受け取ったIDの注文をそのまま返していて、ログイン中のユーザーがその注文の持ち主かどうかを確認していません。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public function show(int $orderId): JsonResponse { $order = Order::with('user')->findOrFail($orderId); return response()->json([ 'id' => $order->id, 'total_amount' => $order->total_amount, 'status' => $order->status, 'customer' => [ 'name' => $order->user->name, 'email' => $order->user->email, ], 'issued_at' => $order->created_at?->toDateString(), ]); } |
つまり、ログインさえしていれば
{orderId}
を他人の注文IDに差し替えるだけで、その人の氏名・メールアドレス・請求額が読めてしまいます。「ログインしているか」は見ているのに、「その人が見てよいデータか」を見ていない、という抜けです。
SQLの組み立ても危険な出力もなく、コードだけを眺めても不備と分からないのが、前の2つとの違いです。
スキャン結果
スキャンが終わると
CLAUDE-SECURITY-<timestamp>/
というディレクトリが作られ、mdファイルで結果が生成されます。
詳細は省きますが、仕込んだ脆弱性はすべて検出されていました
| 1 2 3 4 5 | F1 — 請求書エンドポイントが所有者チェックなしに任意の注文(および所有者の氏名・メール)を返す (HIGH, confidence high) F4 — 生の DB::select の WHERE 句に keyword クエリパラメータが無エスケープで埋め込まれている SQL インジェクション (HIGH, confidence high) F7 — 反射型 XSS: 未認証の ?body= が {!! !!} で生出力される (MEDIUM, confidence high) |
パッチ生成
メニューから Suggest patches を選び、レポートのどの検出を直すかを指定するとパッチが生成されます。
生成されたパッチは、先ほどのディレクトリに
patches/F.patch
として検出ごとに1ファイルずつ、変更内容の説明メモとセットで置かれます。
検証3で仕込んだ認可不備(F1)に対して生成されたパッチが、こちらです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | --- a/app/Http/Controllers/VulnDemo/InvoiceController.php +++ b/app/Http/Controllers/VulnDemo/InvoiceController.php use App\Http\Controllers\Controller; use App\Models\Order; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class InvoiceController extends Controller { /** - * Return the invoice payload for a single order. + * Return the invoice payload for a single order owned by the caller. */ - public function show(int $orderId): JsonResponse + public function show(Request $request, int $orderId): JsonResponse { - $order = Order::with('user')->findOrFail($orderId); + $order = Order::with('user') + ->where('user_id', $request->user()->id) + ->findOrFail($orderId); |
引数に
$request
を追加してログインユーザーのIDを取得し、そのIDの注文だけに絞り込んでから取得する修正になっています。他人の注文は「見つからない」扱いになるので、IDを変えながら他人の請求書を覗く、という手が使えなくなります。
手動でパッチ適用します。
| 1 | git apply CLAUDE-SECURITY-<timestamp>/patches/F1.patch |
さいごに
- 一部スキャンからパッチ生成/適用が完了するまで3h近く掛かりました。大きいリポジトリの全スキャンとなると、結構な時間が掛かると予想されるのでプロジェクトの節目のタイミング等に行なう感じになるかと思います。