Claude Codeで脆弱性診断の報告書を再現してから直す — devcontainerで隔離する手順
診断報告書の再現手順を、書き込み範囲と通信先を絞ったdevcontainerで実行し、修正前後で同じスクリプトの結果を比べる手順を設定例つきで示します。
診断報告書の指摘を直すときは、先に再現して「確かに起きる」を確かめ、直したあとに同じ手順で「起きない」を確かめます。この往復をClaude Codeに任せるなら、実行場所をdevcontainerに隔離し、書き込み範囲と通信先を絞るのが出発点です。
ここでの対象は、自分たちが開発しているアプリを手元のコンテナで起動した状態に限ります。外部のサービスや他人の環境に向けて再現手順を打つ作業は含みません。
再現と修正を同じスクリプトで挟む
報告書の「再現手順」は、人が読むと曖昧になりがちです。「../ を含むパスでファイルが読める」と書かれていても、どのURLに何を送り、何が返れば「再現」とみなすのかは読み手ごとにずれます。
そこで再現手順を、成否を終了コードで返すスクリプトに落とします。再現できたら非ゼロ、再現しなければゼロです。修正の前後で同じスクリプトを回し、結果が反転することを確かめます。
再現から修正確認までの流れ
- 1
コンテナを作る
診断用のdevcontainerを用意し、ホストの秘密情報を入れません。
- 2
再現スクリプトを書く
報告書の指摘1件につき1本、人が書いて
repro/に置きます。再現したら非ゼロで終わります。 - 3
修正前に失敗を確認する
直す前にスクリプトが「再現した」と返すことを見ます。
- 4
直して同じスクリプトを回す
終了コードがゼロに変われば、その手順では通らなくなったと言えます。
- 5
修正を一時的に戻して確かめる
戻した状態で再び非ゼロになれば、スクリプトが検出力を持つ証拠です。
5番目を省くと、スクリプト自体が壊れていて常にゼロを返していても気づけません。修正の効果と検査の健全さは、別々に確かめる必要があります。
診断用のdevcontainerを組む
devcontainerには、Claude Code用のDev Container Featureを足します。.devcontainer/devcontainer.json の最小形は次のとおりです。
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
},
"remoteUser": "vscode",
"mounts": [
"source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
}
}image はプロジェクトのベースイメージに置き換えます。末尾の :1.0 はFeatureのインストールスクリプトの版で、Claude Code本体の版ではありません。Featureは常に最新のClaude Codeを入れ、コンテナ内では既定で自動更新も働きます。診断の途中で挙動が変わると前後比較の意味が薄れるため、版を固めたいときはDockerfileで npm install -g @anthropic-ai/claude-code@X.Y.Z を使い、DISABLE_AUTOUPDATER を 1 にします。
マウントは設定の永続化用です。CLAUDE_CONFIG_DIR を同じパスに向けておくと、~/.claude.json もボリューム側に書かれ、再構築のたびに初期設定へ戻りません。名前に ${devcontainerId} を入れると、プロジェクトごとに状態が分かれます。
入れてはいけないものを決める
ホストの ~/.ssh やクラウドの認証情報ファイルは、マウントしません。ドキュメントは、--dangerously-skip-permissions で動かすと、悪意あるプロジェクトがコンテナ内で触れるものを外へ持ち出せると警告しています。持ち出せる対象には、~/.claude に保存されたClaude Codeの認証情報も含まれます。
クラウドプロバイダーを使うなら、認証情報はファイルでなく containerEnv やCodespacesのシークレット、ワークロードIDで渡します。診断用の環境は短命なトークンで足ります。
書き込み範囲と通信先を絞る
コンテナの中でも、Claude Codeのサンドボックスを重ねて使えます。サンドボックスは既定で無効なので、/sandbox で有効にするか、設定ファイルの sandbox.enabled を true にします。
既定で、サンドボックス内のコマンドが書けるのは作業ディレクトリ、ユーザーごとの一時ディレクトリ、--add-dir などで足したディレクトリです。読み取りはマシンのほぼ全体に及び、~/.ssh や ~/.aws/credentials のような認証情報ファイルも含みます。通信は手元のプロキシ経由で、許可ドメインは空から始まります。
再現作業向けの設定は次の形です。プロジェクトの .claude/settings.json に置きます。
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["."],
"denyWrite": ["./repro"]
},
"network": {
"allowedDomains": []
}
}
}denyReadとallowReadの組み合わせで、ホームの外側は読めず、プロジェクトだけが読めます。プロジェクト設定に置くと.はプロジェクトルートを指しますdenyWriteでrepro/を守り、再現スクリプトを後から書き換えられないようにしますallowedDomainsを空にしておくと、新しい宛先への接続は承認待ちか、モードによって拒否になります
書き込みを広げたいときは allowWrite に具体的なパスを足します。書式の細部はallowWriteとdenyWriteの調整で扱っています。
サンドボックスが守らない範囲
守備範囲には限界があります。サンドボックスが包むのはシェルコマンドです。ReadやEditのような組み込みツールは、サンドボックスではなく権限ルールに従います。フック、ローカルのMCPサーバー、LSPサーバーも、サンドボックスの外で動きます。
したがって、再現スクリプトの書き換えを止めるには、denyWrite に加えて権限ルール側にも手当てが要ります。
{
"permissions": {
"deny": ["Edit(/repro/**)"]
}
}/repro/** は、プロジェクト設定ではプライマリの作業ディレクトリからの相対位置を指します。Claude Codeは、設定ファイル類や .mcp.json などへの書き込みもサンドボックス側で既に拒否します。ただし repro/ までは自動で守りません。
通信を締める位置を決める
通信の制限には、層が二つあります。
通信を締める二つの層
サンドボックスの許可リスト
Bashで動くコマンドの通信先をホスト名で絞ります。allowedDomains に加え、strictAllowlist を true にすると、リスト外を確認なしで拒否します。リポジトリの設定ファイルに書いても効かず、ユーザー設定、管理設定、--settings で渡す必要があります。v2.1.219以降です。
参照構成のファイアウォール
init-firewall.sh が外向き通信を許可先だけに絞ります。NET_ADMIN と NET_RAW が必要で、Claude Code自体には不要です。サンドボックスが包まないプロセスの通信も止められます。
診断中にフックやMCPサーバーを動かさないなら、前者で足ります。動かすなら、後者も併用すると穴が小さくなります。ファイアウォールの中身はinit-firewall.shの解説にあります。
一度きりの試行なら、設定ファイルを書かずに起動時に渡す方法もあります。
claude --settings '{"sandbox": {"enabled": true,
"network": {"strictAllowlist": true}}}'コンテナ内のサンドボックスで詰まる点
bwrapが起動しない
権限を絞ったコンテナでは、サンドボックスの実体であるbubblewrapが新しい /proc をマウントできません。コマンドが Can't mount proc on /newroot/proc: Operation not permitted のようなエラーで落ちます。
回避策は sandbox.enableWeakerNestedSandbox を true にすることです。コンテナの既存の /proc をそのまま使うので、サンドボックス内のコマンドにプロセス情報が見えるようになります。外側のコンテナが十分な隔離を提供しているときに限って使う設定です。詳しくはenableWeakerNestedSandboxとbwrapPathを参照してください。
localhostのアプリに届かない
再現でいちばん踏みやすいのがここです。Linuxでは、サンドボックス内のコマンドから見た localhost は、そのコマンド専用です。ホストや別のプロセスが立てたサーバーには、直接つながりません。allowLocalBinding もLinuxでは効きません。
回避の考え方は単純です。同じコマンドの中で、対象アプリを起動し、リクエストを送り、止めるところまでを行います。コマンド自身が起動したサーバーには届くので、再現スクリプトの設計と相性が良く、excludedCommands でサンドボックスの外へ出す必要がありません。
再現スクリプトの書き方
次の例は、パストラバーサルの指摘を想定した一例です。URL、起動コマンド、判定文字列は自分のアプリに合わせて置き換えます。
#!/usr/bin/env bash
# repro/F-03.sh 指摘F-03: ../ を含むパスで設定ファイルが読める
# 再現したら exit 1、再現しなければ exit 0
set -u
npm start >/tmp/app.log 2>&1 &
APP_PID=$!
trap 'kill "$APP_PID" 2>/dev/null' EXIT
for _ in $(seq 1 30); do
curl -fs http://127.0.0.1:3000/healthz >/dev/null && break
sleep 1
done
body=$(curl -s --path-as-is \
'http://127.0.0.1:3000/files/..%2f..%2fconfig/app.yml')
if grep -q 'db_password' <<<"$body"; then
echo "REPRODUCED: F-03"
exit 1
fi
echo "NOT REPRODUCED: F-03"
exit 0終了コードで返すのが肝です。出力の文面を人が読んで判断する形にすると、Claudeが「それらしく直った」と誤読する余地が残ります。
CLAUDE.mdで進め方を固定する
Claude側には、手順をCLAUDE.mdで固定して渡します。
## 診断指摘の再現と修正
- 再現スクリプトは人が `repro/F-xx.sh` に置く。Claudeは実行だけ行う。再現したら exit 1、しなければ exit 0
- 修正前に必ず実行し、exit 1 を出力ごと報告する。exit 0 なら修正に進まない
- 修正後に同じスクリプトを実行し、exit 0 を確認する
- `repro/` 配下は編集しない。スクリプトの不備は報告だけする
- 外部ホストへ通信しない。対象は 127.0.0.1 のこのアプリだけ「exit 0なら修正に進まない」の1行が効きます。環境の違いで再現しない指摘を、勘で直してしまう動きを止められるからです。報告書の記載と手元の挙動が食い違うこと自体が、報告書の著者に返す情報になります。
修正前後の確認
指示は短く、1件ずつ出します。
repro/F-03.sh を実行して結果を報告して。まだ直さないで。exit 1を確認したら、修正を依頼します。直したあとで同じスクリプトを回し、exit 0になれば、その手順では通らなくなったことになります。最後に、手順5の逆確認を行います。
git stash push -- src/
bash repro/F-03.sh; echo "exit=$?"
git stash pop修正を一時的にしまった状態でexit 1が出れば、スクリプトは検出力を保っています。共有のworktreeで作業しているなら、git stash の代わりに修正前のコミットをチェックアウトした別worktreeでも同じ確認ができます。
何が確かめられて、何が確かめられないか
この手順で言えるのは「報告書の再現手順は、修正後の手元の環境では通らなくなった」までです。次のことは言えません。
- 別の経路で同じ脆弱性が突けないこと。再現手順は1つの入り口に過ぎません
- 本番環境の構成差、たとえばリバースプロキシのパス正規化の違いに左右されないこと
- 診断者の環境で通らないこと。再テストは診断側に依頼します
Trivyで脆弱性を直す手順のように、ツールが検出を自動で繰り返せる指摘は、再スキャンで閉じられます。手動診断の指摘は検出器がないので、このスクリプトが検出器の代わりです。スキャン結果の優先度付けは是正対応表にまとめる方法で別に整理できます。
無人で回すときの境界
--dangerously-skip-permissions で承認なしに回す場合は、前提が増えます。このフラグはroot起動では拒否されるため、remoteUser は非rootにします。ワークスペースはホストにバインドされているので、コンテナ内の変更はそのままホストのリポジトリに現れます。通信制限と併用しないと、権限確認を外した状態で外へ届いてしまいます。
チームで運用するなら、permissions.disableBypassPermissionsMode を管理設定で "disable" にすれば、このフラグ自体を使えなくできます。コンテナ内にも /etc/claude-code/managed-settings.json を置けますが、Dockerfileはリポジトリの一部なので、書き込み権限があれば外せます。外せない方針にしたい場合は、サーバー管理設定かMDMで配ります。
隔離方式の選び方全体はサンドボックスの選び方にまとめています。
よくある質問
再現できない指摘はどう扱うか
前提条件の差を探すときは、診断時に使われたテストデータ、手前に立つミドルウェア、ログイン済みかどうかの状態を、報告書の記載と手元のコンテナで見比べます。初期データや認証状態が足りないせいで通らないだけの例が典型です。差を埋めてもexit 0のままなら、その条件を添えて診断者に返します。
スクリプトにClaudeを使わず人が書いてもよいか
この記事の手順では、人が書く形が前提です。repro/ は denyWrite と Edit の拒否ルールで書き込み禁止にしてあり、Claudeはスクリプトを作れないためです。判定の根拠が人の手元に残るので、監査にも向きます。Claudeに下書きさせたい場合は、その作業の間だけ2つの拒否設定を外し、人がレビューしてコミットしてから戻します。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。