DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでContent Security Policyを設計する:XSS防止・nonce・Report-Only移行

はじめに

Content Security Policy(CSP)はXSSを根本から防ぐブラウザ側のセキュリティ機構だ。しかし設定ミスでサイト全体が壊れるリスクもある。Claude Codeを使って、nonceベースのCSP設計から安全な段階的移行まで体系的に組み立てる方法を解説する。

リクエスト毎nonceの生成(16バイトランダム)

CSPのscript-srcをnonceベースにすることで、インラインスクリプトの実行を厳密に制御できる。nonceはリクエスト毎に生成し、推測不可能な16バイトのランダム値を使う。

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();

  // テンプレートエンジンからnonceを参照できるように
  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();
};
Enter fullscreen mode Exit fullscreen mode

Claude Codeへの指示例:「Express用CSPミドルウェアを作って。nonceは毎リクエスト16バイトで生成し、res.localsに格納してテンプレートから使えるようにして」

strict-dynamicでスクリプト伝播を制御

'strict-dynamic'を使うことで、nonce付きスクリプトから動的に読み込まれるスクリプトも信頼できる。CDNや動的ロードを使うモダンなSPAには必須の設定だ。

<!-- HTMLテンプレート側(EJS例) -->
<!DOCTYPE html>
<html>
<head>
  <!-- nonce付きでインラインスクリプトを許可 -->
  <script nonce="<%= nonce %>">
    // このスクリプトから動的に読み込まれるものも信頼される(strict-dynamic)
    import('/app.js');
  </script>

  <!-- 外部スクリプトもnonce必須 -->
  <script src="/bundle.js" nonce="<%= nonce %>"></script>
</head>
Enter fullscreen mode Exit fullscreen mode
// script-srcの構成例
const scriptSrc = [
  `'nonce-${nonce}'`,      // このnonce付きスクリプトを信頼
  `'strict-dynamic'`,      // + そこから動的ロードされるものも信頼
  // 'unsafe-inline' は strict-dynamic があれば古いブラウザのフォールバック
].join(' ');
Enter fullscreen mode Exit fullscreen mode

Report-Only 2週間 → Enforce への移行

いきなりEnforceすると既存機能が壊れる。Content-Security-Policy-Report-Onlyで2週間レポートを収集し、問題がないことを確認してからEnforceに切り替える。

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モードで運用
Enter fullscreen mode Exit fullscreen mode

Claude Codeへの指示例:「CSPをReport-OnlyとEnforceで切り替えられるように環境変数でモード管理して」

違反レポートエンドポイントとブラウザ拡張フィルタリング

report-uriに送られてくる違反レポートの大部分はブラウザ拡張機能によるものだ。ノイズを除去するフィルタリングが必要になる。

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();
};
Enter fullscreen mode Exit fullscreen mode

まとめ

  • リクエスト毎noncecrypto.randomBytes(16)で推測不可能な値を生成し、インラインスクリプトを厳密に制御する
  • strict-dynamic:nonce付きスクリプトからの動的ロードも信頼でき、モダンSPAのCSP対応が現実的になる
  • Report-Only 2週間:いきなりEnforceせず、レポート収集で安全に移行する
  • 拡張機能フィルタリング:違反レポートのノイズを除去し、本物の問題だけを検知する

Claude Codeにセキュリティ要件を伝えれば、nonce生成・ミドルウェア・レポート処理まで一貫して生成できる。


みょうがのSecurity Pack(¥1,480)では、CSP・OWASP Top 10対応のセキュリティレビュー用Claude Codeカスタムスキルを提供しています。
Security Pack を見る →

Top comments (0)