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

Published: (March 11, 2026 at 02:52 AM EDT)
4 min read
Source: Dev.to

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‑2Report-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();
};

まとめ

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

Security Pack の紹介

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

Security Pack を見る →

0 views
Back to Blog

Related posts

Read more »