Claude CodeでContent Security Policyを設計する:XSS防止・nonce・Report-Only移行
Source: Dev.to
はじめに
Content Security Policy(CSP)は XSS を根本から防ぐブラウザ側のセキュリティ機構です。設定ミスでサイト全体が壊れるリスクもあるため、nonce ベースの CSP 設計から安全に段階的に移行する手順を解説します。
nonce ベースの CSP ミドルウェア(Express)
import { randomBytes } from 'crypto';
import { RequestHandler } from 'express';
// リクエスト毎に 16 バイトの nonce を生成
const generateNonce = (): string => randomBytes(16).toString('base64');
export const cspMiddleware: RequestHandler = (req, res, next) => {
const nonce = generateNonce();
// テンプレートエンジンから参照できるように
res.locals.nonce = nonce;
const cspDirectives = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"default-src 'self'",
"style-src 'self' 'unsafe-inline'", // CSS 移行は別途対応
"img-src 'self' data: https:",
"connect-src 'self'",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
`report-uri /csp-report`,
].join('; ');
// まず Report-Only で様子を見る
res.setHeader('Content-Security-Policy-Report-Only', cspDirectives);
next();
};
ポイント
nonceはリクエスト毎に生成し、推測不可能な 16 バイトのランダム値を使用。'strict-dynamic'を併せて指定することで、nonce 付きスクリプトから動的に読み込まれるスクリプトも信頼できます(モダン SPA に必須)。
HTML テンプレート側(EJS 例)
">
// このスクリプトから動的に読み込まれるものも信頼される(strict-dynamic)
import('/app.js');
">
…
script-src の構成例
const scriptSrc = [
`'nonce-${nonce}'`, // この nonce 付きスクリプトを信頼
`'strict-dynamic'`, // そこから動的ロードされるものも信頼
// 'unsafe-inline' は strict-dynamic があれば古いブラウザのフォールバック
].join(' ');
段階的な移行フロー
const CSP_MODE = process.env.CSP_MODE ?? 'report-only'; // 'enforce' or 'report-only'
export const cspMiddleware: RequestHandler = (req, res, next) => {
const nonce = generateNonce();
res.locals.nonce = nonce;
const directives = buildCspDirectives(nonce);
if (CSP_MODE === 'enforce') {
// 2 週間後にこちらへ切り替え
res.setHeader('Content-Security-Policy', directives);
} else {
// まず Report-Only で違反を収集
res.setHeader('Content-Security-Policy-Report-Only', directives);
}
next();
};
移行スケジュール例
| 期間 | 内容 |
|---|---|
| Week 1‑2 | Report-Only で違反レポートを収集・分析 |
| Week 3 | 違反がゼロであることを確認 → CSP_MODE=enforce に変更 |
| Week 4+ | Enforce モードで運用 |
CSP 違反レポートのハンドリングとノイズ除去
interface CspReport {
'csp-report': {
'document-uri': string;
'blocked-uri': string;
'violated-directive': string;
'source-file'?: string;
'script-sample'?: string;
};
}
const EXTENSION_PATTERNS = [
/^chrome-extension:/,
/^moz-extension:/,
/^safari-extension:/,
/^ms-browser-extension:/,
];
export const cspReportHandler: RequestHandler = (req, res) => {
const report = req.body as CspReport;
const csp = report['csp-report'];
const blockedUri = csp['blocked-uri'] ?? '';
const sourceFile = csp['source-file'] ?? '';
// ブラウザ拡張機能による違反はスキップ(ノイズ除去)
const isExtension = EXTENSION_PATTERNS.some(
(p) => p.test(blockedUri) || p.test(sourceFile)
);
if (!isExtension) {
// 本物の違反のみログ記録
console.warn('CSP violation:', {
blockedUri,
violatedDirective: csp['violated-directive'],
documentUri: csp['document-uri'],
});
// Datadog や Sentry に送る例
metrics.increment('csp.violation', {
directive: csp['violated-directive'],
});
}
res.status(204).end();
};
まとめ
- リクエスト毎 nonce:
crypto.randomBytes(16)で推測不可能な値を生成し、インラインスクリプトを厳密に制御。 - strict-dynamic:nonce 付きスクリプトからの動的ロードも信頼でき、モダン SPA の CSP 対応が現実的になる。
- Report-Only で 2 週間:いきなり Enforce せず、レポート収集で安全に移行。
- 拡張機能フィルタリング:違反レポートのノイズを除去し、本物の問題だけを検知。
Security Pack の紹介
みょうがの Security Pack(¥1,480)では、CSP・OWASP Top 10 対応のセキュリティレビュー用 Claude Code カスタムスキルを提供しています。