Claude CodeでWebセキュリティヘッダーを設定する実践ガイド|CSP・nonce・HSTS・広告衝突対策
Claude CodeでCSP、nonce、HSTS、frame-ancestorsを安全に設計し、Next.js/Astro/Express/Cloudflareで検証する実践ガイド。
Webセキュリティヘッダーは、地味ですが公開サイトの守りを大きく左右します。ログイン画面、管理画面、広告を貼ったブログ、外部CDNを使うLPでは、1つのヘッダー不足がXSS、クリックジャッキング、参照元URLの漏えい、意図しないカメラ・位置情報許可につながります。
ただし、初心者がいきなり「強いCSPを入れよう」とすると、Next.jsのハイドレーションが止まる、Google Analyticsが計測されない、AdSense広告が消える、画像CDNが読めない、HSTS preloadで古いサブドメインを壊す、という事故も起きます。Webセキュリティヘッダーは「全部を最強にする」作業ではなく、「自分のサイトが本当に読み込むものを棚卸しし、段階的に狭める」作業です。
この記事では、Claude Codeに任せる前提で、CSP、nonce、HSTS、X-Frame-Optionsとframe-ancestors、Referrer-Policy、Permissions-Policyを実装レベルで整理します。Next.js、Astro、Express、Cloudflare Pagesの設定例、CSP report、Security HeadersやCSP Evaluatorでの検証、広告・Analytics・画像CDNとの衝突まで扱います。Claude Codeの安全な使い方全体はClaude Codeセキュリティベストプラクティス、計測設計はアナリティクス実装ガイドも合わせて読むと実務に落とし込みやすいです。
公式情報は必ず原典で確認してください。基準にしたのはMDN Content-Security-Policy、Next.js CSP guide、MDN Strict-Transport-Security、hstspreload.org、MDN X-Frame-Options、MDN Referrer-Policy、MDN Permissions Policy、Cloudflare Pages Headers、Helmet、Google Tag ManagerのCSPガイド、AdSenseのCSPガイドです。
まずClaude Codeに渡す前提を決める
Claude Codeは設定ファイルを一気に書けますが、前提が曖昧だと危険な緩和策を入れがちです。たとえば「CSPエラーを直して」とだけ頼むと、script-src * 'unsafe-inline' 'unsafe-eval'のような広すぎる設定が提案されることがあります。これはエラーは消えますが、CSPの価値も消えます。
最初に、次のような棚卸しをしてください。
| 確認項目 | 例 | 判断 |
|---|---|---|
| フレーム埋め込み | 管理画面は埋め込ませない、YouTubeだけ埋め込む | frame-ancestorsとframe-srcを分ける |
| スクリプト | Next.js本体、GTM、AdSense、決済SDK | nonceかhashを優先し、常時unsafe-inlineは避ける |
| 画像 | 自サイト、OGP、Cloudinary、S3、Google広告 | img-srcを広げる理由を記録する |
| 通信先 | API、GA4、Sentry、決済、CSP report | connect-srcに反映する |
| HTTPS | 全サブドメインがHTTPSか | HSTSのincludeSubDomainsとpreloadを慎重に扱う |
Claude Codeには、実装の前に次のプロンプトを渡すと安定します。
このリポジトリのWebセキュリティヘッダーを設計してください。
条件:
- まず現状のscript/img/connect/frame/fontの外部ドメインを棚卸しする
- CSPはReport-Onlyから始め、危険な * と常時 unsafe-inline を避ける
- Next.jsでnonceが必要な場合は動的レンダリングへの影響を書く
- HSTS preloadはデフォルトで入れず、段階的なmax-ageを提案する
- Google Analytics、GTM、AdSense、画像CDNとの衝突を確認する
- 最後にcurl、Security Headers、CSP Evaluatorでの検証手順を出す
この指示のポイントは、Claude Codeに「設定値」ではなく「調査、設計、検証」の順番を守らせることです。セキュリティヘッダーは1回で完璧にするより、Report-Onlyで実リクエストを見て、影響が出ない範囲を確認してから本番適用する方が安全です。
2026年6月時点の推奨ヘッダー方針
初心者向けに、各ヘッダーを平易に言い換えます。CSPは「ブラウザに読み込み許可リストを渡すルール」です。nonceは「このレスポンスで1回だけ有効な合言葉」です。HSTSは「次回以降は必ずHTTPSで来てくださいとブラウザに記憶させるルール」です。frame-ancestorsは「このページを誰がiframeに入れてよいか」を決めます。Referrer-Policyは「リンク先にどこまで元URLを教えるか」を決めます。Permissions-Policyは「カメラ、マイク、位置情報などのブラウザ機能をページやiframeに使わせるか」を決めます。
実務の初期値は次のように考えると事故が少ないです。
| ヘッダー | 初期方針 | 注意点 |
|---|---|---|
Content-Security-Policy | まずContent-Security-Policy-Report-Onlyで観測 | *で逃げない。広告やGTMはnonce前提にする |
Strict-Transport-Security | 最初はmax-age=300; includeSubDomains程度で試す | preloadは全サブドメイン確認後。戻すのが遅い |
X-Frame-Options | DENYまたはSAMEORIGIN | 現代の主役はCSPのframe-ancestors。ALLOW-FROMは使わない |
frame-ancestors | 管理画面は'none'、埋め込みページだけ許可 | frame-srcとは別物 |
Referrer-Policy | strict-origin-when-cross-origin | URLに個人情報や検索条件を入れない設計も必要 |
Permissions-Policy | 使わない機能は(), 決済など必要なものだけ許可 | 親frameで禁止した機能は子frameで復活できない |
X-Content-Type-Options | nosniff | 低コストなので基本入れる |
HSTS preloadだけは特に慎重にしてください。hstspreload.orgは、preloadをデフォルトで入れるべきではないと明記しています。理由は単純で、登録後に「古い検証用サブドメインだけHTTPでした」と気づいても、ブラウザ側のリスト反映・削除には時間がかかるからです。まず5分、1週間、1か月のようにmax-ageを伸ばし、アクセスログと売上・問い合わせ・広告表示を見てから判断します。
概念図:CSPは「止める」より「観測して狭める」
コード例が増える前に、全体像を図にします。Claude Codeにはこの流れをそのままレビュー観点として渡すと、設定値の丸写しではなく、環境に合った提案を出しやすくなります。
flowchart LR
A["現状の外部リソースを棚卸し"] --> B["Report-OnlyでCSPを配信"]
B --> C["ブラウザConsoleとCSP reportを確認"]
C --> D["広告・Analytics・画像CDNの必要ドメインを分類"]
D --> E["nonce/hash中心の本番CSPへ移行"]
E --> F["Security HeadersとCSP Evaluatorで検証"]
重要なのは、script-src、img-src、connect-src、frame-srcを混同しないことです。YouTubeをiframeで埋め込むならframe-srcが必要ですが、それは「自分のページを他サイトがiframeに入れてよい」という意味のframe-ancestorsとは違います。Google Analyticsの通信が失敗するなら多くはconnect-srcです。画像CDNが表示されないならimg-srcです。AdSenseやGTMが動かない場合は、単純なドメイン追加ではなくnonce対応が必要になることがあります。
Next.jsでnonce付きCSPを設定する
Next.jsはCSPの難所です。App Routerでnonceを使う場合、Next.js公式ドキュメントはproxy.tsでリクエストごとにnonceを作る方法を示しています。2026年6月時点のNext.js 16系では「Proxy」という名称が使われていますが、古いプロジェクトではmiddleware.tsとして同じ考え方を使っている場合があります。
注意点は、nonceはリクエストごとに変わるため、静的HTMLとして完全にキャッシュする前提と相性が悪いことです。ブログ記事やドキュメントのように静的配信したいページでは、hashベースCSP、外部JSへの分離、Cloudflare側の静的ヘッダーなどを検討します。ログイン後のアプリ、決済画面、管理画面ならnonceの価値が高いです。
// proxy.ts
import { NextRequest, NextResponse } from "next/server";
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const isDev = process.env.NODE_ENV !== "production";
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""} https: http:`,
`style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com`,
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: blob: https:",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com",
"frame-src 'self' https://www.youtube-nocookie.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
"report-uri /api/csp-report",
]
.join("; ")
.replace(/\s{2,}/g, " ")
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("Content-Security-Policy", csp);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=(self)"
);
// 本番初日は短く始める。全サブドメイン確認後に段階的に伸ばす。
response.headers.set("Strict-Transport-Security", "max-age=300; includeSubDomains");
return response;
}
export const config = {
matcher: ["/((?!api/csp-report|_next/static|_next/image|favicon.ico).*)"],
};
layout.tsx側でGTMなどにnonceを渡す場合は、Next.jsのheaders()からx-nonceを読みます。GTM公式ガイドもnonceを推奨しています。AdSenseもCSPを使う場合は、ドメインの allowlist だけではなくstrict CSP、つまりnonce方式を前提にした案内を出しています。
// app/layout.tsx
import { GoogleTagManager } from "@next/third-parties/google";
import { headers } from "next/headers";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<html lang="ja">
<body>
{children}
<GoogleTagManager gtmId="GTM-XXXXXXX" nonce={nonce} />
</body>
</html>
);
}
ここでの落とし穴は、開発環境のunsafe-evalを本番に残すことです。Reactの開発用デバッグではunsafe-evalが必要になる場面がありますが、本番CSPに残すべきではありません。また、style-src 'unsafe-inline'を永久に入れると、CSPの効果がかなり弱くなります。CSS-in-JSやインラインstyleの事情がある場合も、最終的にはnonce対応、外部CSS、ライブラリ側のCSP対応を確認してください。
CSP reportを受け取り、Report-Onlyから始める
いきなりContent-Security-Policyを本番で有効にすると、広告、問い合わせフォーム、決済、ログインが止まる可能性があります。最初はContent-Security-Policy-Report-Onlyで配信し、ブラウザに「違反を報告するが、ブロックはしない」状態を作ります。
Next.js Route Handlerなら、最低限の受け口は次のように作れます。
// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const contentType = request.headers.get("content-type") ?? "";
const body = await request.text();
const isCspReport =
contentType.includes("application/csp-report") ||
contentType.includes("application/reports+json") ||
body.includes("violated-directive");
if (!isCspReport) {
return NextResponse.json({ ok: false, reason: "unsupported report" }, { status: 415 });
}
console.warn("csp-report", body.slice(0, 4000));
return new NextResponse(null, { status: 204 });
}
本番では、ログにURL、違反directive、blocked URI、user agent、発生回数を保存し、個人情報を保存しない設計にしてください。report-uriは古い扱いですが、互換性のためにまだ使われます。将来的にはReporting APIのreport-toも検討できますが、2026年6月時点では「report-uriも併用する」判断が現実的です。
Claude Codeには「CSP reportのログを分類して、必要なドメイン、不要なノイズ、攻撃らしいものを分けて」と頼むと役に立ちます。ただし、報告されたドメインを全部追加してはいけません。ブラウザ拡張機能、社内プロキシ、古いタグ、攻撃試行も混ざります。
Astro、Express、Cloudflareでの設定例
Astroは静的サイトにもSSRにも使われるため、nonceを使うかどうかを先に決めます。静的配信中心なら、Cloudflare Pagesの_headersやAstro middlewareで固定ヘッダーを入れ、インラインスクリプトを減らす方が扱いやすいです。
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
const SECURITY_HEADERS: Record<string, string> = {
"Content-Security-Policy-Report-Only": [
"default-src 'self'",
"script-src 'self' https://www.googletagmanager.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: blob: https:",
"connect-src 'self' https://www.google-analytics.com",
"frame-src 'self' https://www.youtube-nocookie.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"report-uri /api/csp-report",
].join("; "),
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(self)",
};
export const onRequest = defineMiddleware(async (_context, next) => {
const response = await next();
for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
response.headers.set(name, value);
}
return response;
});
ExpressではHelmetを使うのが現実的です。Helmetは多くのヘッダーをまとめて扱えますが、CSPはアプリごとの差が大きいため、必ず自分の外部リソースに合わせて調整します。
// server.js
import crypto from "node:crypto";
import express from "express";
import helmet from "helmet";
const app = express();
const isDev = process.env.NODE_ENV !== "production";
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.cspNonce}'`,
"'strict-dynamic'",
"https:",
"http:",
...(isDev ? ["'unsafe-eval'"] : []),
],
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", "https:"],
connectSrc: ["'self'", "https://www.google-analytics.com", "https://analytics.google.com"],
frameSrc: ["'self'", "https://www.youtube-nocookie.com"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
reportUri: ["/csp-report"],
upgradeInsecureRequests: [],
},
},
strictTransportSecurity: {
maxAge: 300,
includeSubDomains: true,
},
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
xFrameOptions: { action: "deny" },
})
);
app.use((req, res, next) => {
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(self)");
next();
});
app.post("/csp-report", express.text({ type: ["application/csp-report", "application/reports+json", "*/*"] }), (req, res) => {
console.warn("csp-report", String(req.body).slice(0, 4000));
res.status(204).end();
});
app.listen(3000);
Cloudflare Pagesでは_headersファイルで静的に設定できます。ただし、静的な_headersではリクエストごとのnonceを生成できません。nonceが必要なサイトは、Workers/Functions、SSR、またはhashベースCSPを検討します。
# public/_headers または dist/_headers
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https:; connect-src 'self' https://www.google-analytics.com; frame-src 'self' https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; report-uri /csp-report
Cloudflare側でHSTSを設定する場合、アプリ側と二重に出さないようにしてください。二重ヘッダーは検証ツールで読みにくく、どちらが実際に効いているか分かりにくくなります。
3つ以上のユースケースで考える
ユースケース1: 個人ブログや技術メディア。 画像CDN、Google Fonts、GTM、AdSenseを使うことが多い構成です。ここでは最初からscript-src 'self'だけにすると広告と計測が止まります。GoogleのCSPガイドは、GTMやAdSenseでnonce方式を案内しています。収益化しているメディアでは、CSPを厳しくする前にReport-Onlyで広告表示率、CLS、広告リクエスト、Analyticsイベントを見ます。
ユースケース2: SaaSのログイン後アプリ。 管理画面、請求画面、ユーザー設定画面では、外部スクリプトを減らし、frame-ancestors 'none'、object-src 'none'、base-uri 'self'、form-action 'self'を強くします。決済SDKやサポートチャットを使う場合は、対象ページだけCSPを分ける方が安全です。全ページでPayPalやStripeの許可を広げる必要はありません。
ユースケース3: 埋め込みウィジェットやダッシュボード。 他社サイトにiframeで埋め込ませる画面だけは、frame-ancestorsに許可元を入れます。ここでX-Frame-Options: DENYを全体に付けると埋め込みが壊れます。埋め込み専用ルートと通常ページでヘッダーを分ける設計が必要です。
ユースケース4: 静的LPとCloudflare Pages。 _headersで低コストに始められますが、nonceは生成できません。インラインJSを外部ファイル化し、必要ならhashベースCSPを使います。静的LPでGTMを入れるなら、どのタグが実際に発火するかを棚卸ししてください。タグマネージャーは便利ですが、CSP上は「後から外部スクリプトを増やせる入口」です。
よくある失敗例と落とし穴
失敗例1は、CSPエラーを消すためにdefault-src *を入れることです。これはエラー対応ではなく、保護を無効にしているだけです。最低でもdefault-src 'self'から始め、必要な種類ごとにscript-src、img-src、connect-srcを追加します。
失敗例2は、frame-srcとframe-ancestorsの混同です。frame-srcは「このページがどのiframeを読み込めるか」です。frame-ancestorsは「このページを誰がiframeに入れられるか」です。クリックジャッキング対策はframe-ancestorsです。
失敗例3は、HSTS preloadを初日に入れることです。社内用、検証用、古いwww、メール配信用サブドメインまでHTTPSで動くか確認せずにpreloadを入れると、復旧に時間がかかります。まず短いmax-ageで始め、ログと問い合わせを見ます。
失敗例4は、広告・Analyticsを単なるドメイン許可で済ませることです。AdSenseは利用ドメインが変わる可能性があるため、公式ガイドはstrict CSP、つまりnonce方式を案内しています。GTMもコンテナスニペットがインラインJavaScriptを使うため、nonceの受け渡しが必要です。
失敗例5は、開発環境の例外を本番へ持ち込むことです。unsafe-eval、unsafe-inline、広すぎるhttps:は、なぜ必要かをコメントではなく運用メモに残し、削れるタイミングを決めてください。
検証手順:curl、ブラウザ、外部スキャナ
実装後は、Claude Codeに「設定したので終わり」とさせず、必ず検証します。まずローカルまたはプレビュー環境でヘッダーを見ます。
curl -I https://example.com/
curl -I https://example.com/login
curl -I https://example.com/embed/widget
確認する項目は、CSPが想定ルートに出ているか、Report-Onlyと本番CSPを二重に出していないか、HSTSがHTTPSレスポンスだけに出ているか、埋め込み専用ページでframe-ancestorsが正しいか、広告・AnalyticsがConsoleでブロックされていないかです。
外部検証ではSecurity Headersで全体のヘッダーを確認し、CSP EvaluatorでCSPの弱点を見ます。スコアは便利ですが、A+だけを目的にしないでください。たとえば埋め込みウィジェットを提供するサービスでは、frame-ancestors 'none'が正解ではないページもあります。サイトの機能を理解した上で、ページ単位で評価します。
Claude Codeへの検証プロンプトは次のようにします。
この差分をセキュリティヘッダー観点でレビューしてください。
- CSPのdirectiveごとに、許可が広すぎる箇所を指摘する
- Google Analytics、GTM、AdSense、画像CDN、YouTube iframeが壊れる可能性を確認する
- HSTS preloadを入れる条件を満たしているか確認する
- curlとSecurity Headersで確認すべきURLを3つ以上出す
- 本番適用前にReport-Onlyで見るべきログ項目を列挙する
マネタイズCTAと実際に試した結果
セキュリティヘッダーは、収益化にも直結します。AdSense広告がCSPで止まると収益が落ちます。Analyticsが止まると改善判断ができません。逆に、管理画面や問い合わせフォームの保護が弱いと、信頼を失います。既存サイトのCSP、広告、Analytics、Cloudflare設定を短時間で棚卸ししたい場合は、Claude Code研修・導入相談で、実リポジトリを前提にしたレビュー手順を相談できます。自分で進めたい人は無料チートシートとClaude Code実践テンプレートを使い、ヘッダー設定をCLAUDE.mdのレビュー項目に入れてください。
この記事で紹介した内容を実際に試した結果、最も効果があったのは「最初から本番CSPを入れない」ことでした。検証用のNext.js構成でReport-Onlyを先に出すと、GTMのインラインスニペット、GA4のconnect-src、YouTube埋め込みのframe-src、画像CDNのimg-srcがそれぞれ別の理由で報告されました。Claude Codeにそのログを分類させると、単に許可ドメインを足すのではなく、ページ別CSP、nonce付与、不要タグ削除に分けて修正できました。HSTSも最初はmax-age=300で始めたため、サブドメイン確認の抜けに気づいても安全に戻せました。
まとめると、2026年6月時点で妥当なWebセキュリティヘッダー方針は、CSPをnonceまたはhash中心で設計し、Report-Onlyから観測し、HSTS preloadは慎重に段階導入し、frame-ancestorsとX-Frame-Optionsを役割別に使い、広告・Analytics・CDNの衝突を検証で潰すことです。Claude Codeはこの作業を速くできますが、最終判断は「サイトが実際に読み込むリソース」と「壊してはいけない収益導線」に基づいて行ってください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。