Claude CodeでCSPとセキュリティヘッダーを段階導入する
現状のレスポンスヘッダーを洗い出し、Content-Security-Policy-Report-Onlyで違反ログを集め、許可リストやnonceを詰めてから強制に切り替える手順です。
Content Security Policy(CSP)は、ページが読み込んでよいリソースをブラウザーに伝えるレスポンスヘッダーです。主な用途はクロスサイトスクリプティング(XSS)対策で、スクリプトの読み込み元を絞ります。いきなり強制すると画面が壊れやすいため、違反を記録するだけのReport-Onlyから始めるのが定石です。
この記事では、Claude Codeに「現状ヘッダーの棚卸し → Report-Onlyの設定 → 違反ログの集計 → 強制への切り替え」を手伝わせる流れを順に書きます。ヘッダーの文法はMDNの記述に沿い、Claude Codeに任せる部分と人が判断する部分を分けます。
まず導入するヘッダーを決める
CSPだけがセキュリティヘッダーではありません。MDNの記述から、段階導入の対象になる4つを並べます。
| ヘッダー | 役割 | 設定例 |
|---|---|---|
| Strict-Transport-Security | 役割HTTPSでのアクセスをブラウザーに記憶させる | 設定例max-age=31536000; includeSubDomains |
| X-Content-Type-Options | 役割宣言と異なる種類として中身を解釈させない | 設定例nosniff |
| Referrer-Policy | 役割遷移先へ送る参照元情報の量を決める | 設定例strict-origin-when-cross-origin |
| Content-Security-Policy | 役割読み込めるリソースの種類と場所を決める | 設定例次節以降で詰める |
後ろ3つは設定がほぼ固定です。Referrer-Policyは未指定でも strict-origin-when-cross-origin が既定値になります。つまり明示するのは、既定に頼らず意図を残すためです。
Strict-Transport-Securityは max-age=0 を返すとHSTSが無効になります。preloadの一覧に載せる場合は、max-age を1年(31536000)以上にし、includeSubDomains を付ける必要があります。影響範囲が広いので、CSPとは別の作業として扱うほうが安全です。
CSPの upgrade-insecure-requests はHTTPのリソース参照をHTTPSに上げますが、外部へのリンクまでは上げません。HSTSの代わりにはならず、両方を付ける前提で書かれています。
X-Frame-Options を使っている場合、CSPの frame-ancestors が後継にあたります。埋め込みを許す必要がなければ frame-ancestors 'none' と書きます。
手順1: 現状のレスポンスヘッダーを棚卸しする
最初は何も変更しません。Claude Codeをplan modeで起動すると、読み取りと提案だけで進み、承認するまでファイルを編集しません。
claude --permission-mode plan起動後は、次のように頼みます。
https://staging.example.com/ と主要 3 ページのレスポンスヘッダーを
curl -sI で取得して、次の 4 つの有無と値を表にしてください。
Strict-Transport-Security / X-Content-Type-Options /
Referrer-Policy / Content-Security-Policy
ヘッダーを設定しているコードの場所(nginx 設定、ミドルウェア等)も
探して、変更候補の一覧にしてください。取得対象は本番ではなくステージングを勧めます。理由は2つあります。1つはReport-Onlyの動作確認を本番トラフィックに出す前に済ませられること。もう1つは、Claude Codeに任せるcurlの宛先を絞れることです。
権限の側でも絞れますが、Bash(curl http://github.com/ *) のように引数で縛るルールは、オプションの順序やリダイレクトで外れやすいと権限の説明にあります。宛先の制限はルールに頼らず、依頼文と環境で担保します。
返ってくる表は、たとえば次のような形になります(例示であり、実際の出力ではありません)。
| ヘッダー | トップ | 記事ページ | 判定 |
|---|---|---|---|
| Strict-Transport-Security | トップなし | 記事ページなし | 判定追加 |
| X-Content-Type-Options | トップnosniff | 記事ページnosniff | 判定済み |
| Referrer-Policy | トップなし | 記事ページなし | 判定追加(既定値を明示) |
| Content-Security-Policy | トップなし | 記事ページなし | 判定Report-Onlyで追加 |
CSPは「すべてのリクエストへのすべてのレスポンスに付ける」とMDNにあります。メインのHTMLだけでなく、画像やAPIの応答にも載せる設計です。ミドルウェアで一括付与しているか、パスごとに漏れがないかも棚卸しの項目に入れます。
手順2: Report-Onlyで違反を集める
静的な設定ファイルで完結するヘッダー 3つを足したうえで、CSPは Content-Security-Policy-Report-Only として追加します。ポリシーは強制されず、違反だけが記録されます。同じレスポンスに Content-Security-Policy とReport-Onlyの両方があれば、両方が効きます。強制中のポリシーを残したまま、次の版を試す使い方もできます。
報告先はReporting APIで宣言します。MDNの推奨は、Reporting-Endpoints ヘッダーでエンドポイントに名前を付け、CSPの report-to ディレクティブで指す形です。
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint古い report-uri ディレクティブは非推奨ですが、report-to が全ブラウザーで対応するまでは両方を宣言するようMDNは書いています。ただし report-uri は送信形式とContent-Type(application/csp-report)が異なります。受け口を2種類に対応させるか、report-to だけに絞るかを先に決めます。
最初のポリシーは広めに書きます。たとえば default-src 'self' だけで始めると、違反の大半は「外部のCDN」「インラインのスクリプト」「インラインのイベントハンドラ」として記録されます。どれが本当に必要な読み込みかは、ログを見てから判断します。
受け口を用意する
違反レポートは application/reports+json のPOSTで届きます。MDNによれば、type が "csp-violation" のオブジェクトで、body に次のような項目が入ります。
effectiveDirective: 違反したディレクティブ(script-src-elemなど)blockedURL: 止められた対象(インラインならinline)documentURL: 違反が起きたページdisposition: 強制か報告のみか
受け口の実装は小さくて済みます。ステージング用の例をClaude Codeに作らせるなら、次のような依頼になります。
Express で /csp-reports を受けて、受信した各レポートを
csp-reports.jsonl に 1 行 1 件で追記するだけのハンドラを書いて。
Content-Type は application/reports+json。
ログの保存以外の処理は入れないで。出てくるコードは、おおむね次の形です。
import express from "express";
import { appendFileSync } from "node:fs";
const app = express();
app.post(
"/csp-reports",
express.json({ type: "application/reports+json" }),
(req, res) => {
for (const report of [].concat(req.body)) {
appendFileSync("csp-reports.jsonl", JSON.stringify(report) + "\n");
}
res.sendStatus(204);
}
);
app.listen(3000);本文が1件のオブジェクトでも配列でも動くように、[].concat() で包んでいます。実際の形はステージングで1件受けて確かめてください。
手順3: 違反ログから許可リストを詰める
数日分のログが溜まったら、Claude Codeに集計を頼みます。ここが一番効く作業です。件数が多く、人が目で追うには単調だからです。
集計そのものはjqで十分です。コマンドをClaude Codeに実行させ、結果を読ませます。
jq -r '.body | [.effectiveDirective, .blockedURL] | @tsv' \
csp-reports.jsonl | sort | uniq -c | sort -rn | head -20出力は「件数・ディレクティブ・ブロック対象」の並びになります。上位から順に、次の3つに仕分けます。
- 必要な外部リソース: 解析タグ、フォント、画像CDNなど。該当ディレクティブにホスト名を足します。
- インラインのスクリプトやハンドラ: 許可リストでは解決しません。nonce、hash、またはコードの書き換えが必要です。
- 想定外の読み込み: 拡張機能や、混入したスクリプトの可能性があります。許可せずに調べます。
2番目がMDNの言うStrict CSPの領域です。MDNは、スクリプトの制御にはnonceかhashを使う方式を推奨しています。許可リスト方式は正しく書くのが難しく、外部スクリプトが増えると肥大化するという理由です。同ページには、Googleアナリティクスを組み込むだけで187個のGoogleドメインを許可リストに入れることになった、という事例も引かれています。
nonce方式のポリシーは次の形です。
Content-Security-Policy-Report-Only:
script-src 'nonce-{RANDOM}'; object-src 'none'; base-uri 'none'nonceはレスポンスごとに予測できない値を作り、HTMLの <script nonce="…"> と一致させます。つまりHTMLをテンプレートで動的に出力できることが条件です。静的なHTMLをそのまま配信するサイトでは、スクリプトごとのhashを使います。ただしhash方式は、スクリプトの中身を変えるたびに再計算が必要です。
サードパーティのスクリプトが別のスクリプトを読み込む場合は、'strict-dynamic' を足すと、nonceやhashを持つスクリプトが読み込む先も信頼されます。MDNは同時に、保護が弱まる副作用も書いています。
Claude Codeに守らせる書き方をCLAUDE.mdに残す
違反の大半はインラインのイベントハンドラに由来します。修正の方針を毎回頼むのは手間なので、CLAUDE.mdに短く書いておきます。
## CSP 対応
- HTML に onclick 等のインラインイベントハンドラを書かない。
addEventListener を使う。
- javascript: URL と eval() / new Function() を新規に使わない。
- スクリプトを足すときは nonce 属性の付け方をテンプレートに合わせる。
- script-src に 'unsafe-inline' を足して違反を消さない。インラインハンドラの置き換えは、MDNも addEventListener() への書き換えを例示しています。こうした書き換えは、Claude Codeに「違反ログの上位から順に、該当するテンプレートを直して」と頼める作業です。差分は人が読んで承認します。
'unsafe-inline' は逃げ道ですが、MDNは避けるよう警告しています。しかもnonceやhashが同じディレクティブにあれば、unsafe-inline はブラウザーに無視されます。一方 unsafe-eval はnonceやhashがあっても効き続けます。この差は、エラーが消えない原因を調べるときに知っておくと役立ちます。
手順4: 強制に切り替える
違反ログが落ち着いたら、ヘッダー名を Content-Security-Policy-Report-Only から Content-Security-Policy に変えます。ポリシーの中身はそのままです。切り替え直後は、強制とReport-Onlyを併用する運用もできます。強制するポリシーを実績のあるものにとどめ、次の改訂案はReport-Onlyで試します。
切り替え前の確認は、Claude Codeにチェックリストとして実行させられます。
- ステージングの全主要ページで、コンソールにCSP違反が出ていないか
- 直近のログに、未対応の
blockedURLが残っていないか - 変更したテンプレートが、nonceを毎回新しく出しているか(固定値のまま配信していないか)
- Report-Onlyの受け口が、切り替え後も違反を受けられるか
ログの再集計は、非対話モードで定期実行にもできます。CIに組み込むなら、次のように読み取り系のツールだけを許可します。
claude -p "csp-reports.jsonl の直近の違反を集計して、新しい blockedURL があれば一覧にして" \
--allowedTools "Read,Grep"-p の使い方はclaude -pの解説に、許可ルールの管理は/permissionsコマンドの記事にあります。書き込みを伴う修正は、非対話に任せず手元の対話セッションで承認しながら進めるのが無難です。
つまずきやすい点
- ポリシーの鵜呑み: Claude Codeが出したポリシーに
unsafe-inlineが含まれていたら、そのまま採用しません。許可はReport-Onlyのログで裏付けてから足します。 - レポートが届かない: 受け口のContent-Typeが
application/reports+jsonになっているか、ステージングのURLをReporting-Endpointsが指しているかを確かめます。 - 拡張機能由来のノイズ: 利用者のブラウザー拡張が起こした違反も、ログに混ざります。同じ
blockedURLが特定のページに偏らず散発するなら、まず疑います。 - ブラウザーの対応差:
report-toに未対応の環境向けには、report-uriの併記が必要です。
CSPの考え方は、デスクトップアプリでも出てきます。Electronアプリの開発手順と比べると、Webサイトの場合はサーバーのヘッダーとして運用する点が異なります。
まとめ
進める順序は、現状の棚卸し、Report-Onlyでの収集、ログの仕分け、強制への切り替えです。Claude Codeが担うのは、ヘッダーの取得、ログの集計、テンプレートの書き換え案の作成まで。許可するリソースの最終判断と、強制への切り替え時期は人が決めます。HSTSのpreloadのように戻しにくい設定は、CSPの導入とは別の作業にして、ステージングで挙動を見てから進めてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- developer.mozilla.org/en/docs/Web/HTTP/CSP
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/headless
- code.claude.com/docs/en/permissions
- developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Sec…
- developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Optio…
- developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy