Webセキュリティヘッダーベストプラクティス:CSPからHSTSまでの5つのプロダクションパターン

前端安全

あなたのウェブサイトは「丸腰」かもしれない:セキュリティヘッダー欠落の現実のコスト

2026年4月、あるECプラットフォームがContent-Security-Policyヘッダーの欠落により、第三者CDN経由で悪意のあるスクリプトを注入され、12万件のユーザー決済情報が漏洩しました。事後調査で、わずか1行のレスポンスヘッダー設定でこの攻撃を防げたことが判明しました:

Content-Security-Policy: script-src 'self' 'nonce-abc123'

セキュリティヘッダーはWebセキュリティの「最初の鍵」です。ビジネスコードを1行も変更することなく、サーバー設定に数行のHTTPヘッダーを追加するだけで、XSS、クリックジャッキング、プロトコルダウングレードなど多くの攻撃を防御できます。

現実のデータ:Mozilla Observatoryの2026年スキャンレポートによると、世界Top 10,000ウェブサイトのうち、CSPを正しく設定しているのはわずか31%、HSTS preloadを有効にしているのは18%、少なくとも1つの重要なセキュリティヘッダーが欠落しているサイトは40%以上に上ります。


コア概念クイックリファレンス

ヘッダー フルネーム 防御対象 ブラウザサポート 優先度
CSP Content-Security-Policy XSS、データ注入、コード注入 全プラットフォーム 🔴 Critical
HSTS Strict-Transport-Security プロトコルダウングレード、SSLストリッピング 全プラットフォーム 🔴 Critical
CORS Cross-Origin Resource Sharing クロスオリジンリソースアクセス制御 全プラットフォーム 🔴 Critical
Permissions-Policy Permissions-Policy ブラウザ機能権限制御 Chrome/Edge/Safari 🟡 High
X-Content-Type-Options X-Content-Type-Options MIMEスニッフィング攻撃 全プラットフォーム 🟡 High
X-Frame-Options X-Frame-Options クリックジャッキング 全プラットフォーム(CSPに代替済み) 🟢 Medium
Referrer-Policy Referrer-Policy Referer情報漏洩 全プラットフォーム 🟡 High

問題分析:セキュリティヘッダー設定の5つの課題

課題1:CSPポリシーの複雑さの爆発

CSPには20以上のディレクティブがあり、ディレクティブ間に継承と上書きの関係があります。一つの設定ミスでポリシー全体が無意味になる可能性があります。例えば、script-src'unsafe-inline''unsafe-eval'を使用すると、CSPがないのと同然です。

課題2:HSTS設定ミスによる「自己ロック」

HSTSのmax-ageを長く設定しすぎると、証明書の期限切れやドメイン変更時にユーザーがサイトにアクセスできなくなります。preloadリストからの削除には数週間かかります。

課題3:CORSの「画一的」な設定

多くの開発者が利便性のためにAccess-Control-Allow-Origin: *を設定しますが、これは認証情報を伴うリクエストでは機能せず、CORSメカニズムへの理解不足を露呈します。

課題4:Permissions-Policyとビジネスの競合

geolocationcameraなどの機能を無効にすると、正常なビジネス機能に影響する可能性があります。セキュリティとユーザビリティのバランスを見つけるには、ページごとのきめ細かい設定が必要です。

課題5:マルチサーバー環境の一貫性

Nginx、Caddy、Cloudflare Workers、Vercel Edge — 異なる環境でセキュリティヘッダーの設定方法が異なり、すべてのエントリポイントでポリシーが一貫して適用されることを確認するのはプロダクション環境の中核課題です。


5つのプロダクションパターン:CSPからHSTSまでのセキュリティヘッダー実践

パターン1:Content-Security-Policy(CSP)— ディレクティブ設定、Nonce、Report-Only

CSPはXSS防御に最も効果的なレスポンスヘッダーです。ホワイトリストメカニズムにより、ページがロードできるリソースを制御し、悪意のあるコード注入を根源から遮断します。

基本ディレクティブ設定

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
ディレクティブ 意味 推奨値 説明
default-src すべてのリソースのデフォルトポリシー 'self' 他のディレクティブ未指定時に継承
script-src JavaScriptソース 'self' 'nonce-xxx' 'unsafe-inline'は絶対に使用しない
style-src CSSソース 'self' 'unsafe-inline' インラインスタイルは通常避けられない
img-src 画像ソース 'self' data: https: data URIとHTTPS画像を許可
font-src フォントソース 'self' https://fonts.gstatic.com フォントCDNを制限
connect-src fetch/XHR/WebSocketソース 'self' https://api.example.com API呼び出し先を制限
frame-ancestors 埋め込みソース 'none' X-Frame-Optionsの代替
base-uri <base>タグソース 'self' ベースハイジャックを防止
form-action フォーム送信先 'self' フォームハイジャックを防止
upgrade-insecure-requests HTTPを自動的にHTTPSにアップグレード (値なし) HSTSと併用

NonceベースCSP — プロダクション環境のベストプラクティス

'unsafe-inline'はCSPを無意味にします。Nonce(使い捨て乱数)で代替し、正しいNonceを持つインラインスクリプトのみ実行を許可します。

import crypto from "crypto";

function generateNonce(): string {
  return crypto.randomBytes(16).toString("base64");
}

function applyCSPNonce(res: any, html: string): string {
  const nonce = generateNonce();

  res.setHeader(
    "Content-Security-Policy",
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-${nonce}'`,
      `style-src 'self' 'unsafe-inline'`,
      `img-src 'self' data: https:`,
      `font-src 'self' https://fonts.gstatic.com`,
      `connect-src 'self' https://api.example.com`,
      `frame-ancestors 'none'`,
      `base-uri 'self'`,
      `form-action 'self'`,
    ].join("; ")
  );

  return html.replace(/<script>/g, `<script nonce="${nonce}">`);
}

CSP Report-Only — 安全なデプロイ戦略

CSPを直接デプロイすると既存機能が壊れる可能性があります。まずContent-Security-Policy-Report-Onlyで違反レポートを収集し、問題がないことを確認してから強制モードに切り替えます。

interface CSPViolationReport {
  documentUri: string;
  referrer: string;
  blockedUri: string;
  violatedDirective: string;
  effectiveDirective: string;
  originalPolicy: string;
  disposition: "enforce" | "report";
  statusCode: number;
}

function setupCSPReportOnly(res: any): void {
  res.setHeader(
    "Content-Security-Policy-Report-Only",
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-placeholder'`,
      `style-src 'self' 'unsafe-inline'`,
      `img-src 'self' data: https:`,
      `connect-src 'self' https://api.example.com`,
      `frame-ancestors 'none'`,
      `report-uri /api/csp-report`,
    ].join("; ")
  );
}

import express from "express";

const app = express();

app.use(express.json({ type: "application/csp-report" }));

app.post("/api/csp-report", (req, res) => {
  const report: CSPViolationReport = req.body["csp-report"];
  console.warn("[CSP Violation]", {
    blockedUri: report.blockedUri,
    violatedDirective: report.violatedDirective,
    documentUri: report.documentUri,
  });
  res.status(204).end();
});

パターン2:HSTSと証明書セキュリティ — Preload、サブドメイン、Max-Age戦略

HSTSはブラウザに「このサイトにはHTTPS経由でのみアクセス可能」と伝え、SSLストリッピング攻撃とプロトコルダウングレードを防止します。

HSTS 3レベル設定

Strict-Transport-Security: max-age=31536000
Strict-Transport-Security: max-age=31536000; includeSubDomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
設定レベル ディレクティブ 効果 リスク 適用シーン
Level 1 max-age=31536000 現在のドメインが1年間HTTPSのみ 初期デプロイ
Level 2 + includeSubDomains すべてのサブドメインもHTTPSのみ 全サブドメインのHTTPS対応確認後
Level 3 + preload ブラウザ内蔵HSTSリストに追加 大規模プロダクションサイト

HSTS段階的デプロイ戦略

class HSTSDeploymentManager {
  private currentStage: number = 1;
  private readonly stages = [
    { maxAge: 300, includeSubDomains: false, preload: false, description: "5分テスト" },
    { maxAge: 86400, includeSubDomains: false, preload: false, description: "1日検証" },
    { maxAge: 604800, includeSubDomains: false, preload: false, description: "1週間安定" },
    { maxAge: 2592000, includeSubDomains: false, preload: false, description: "30日確認" },
    { maxAge: 31536000, includeSubDomains: false, preload: false, description: "1年本番" },
    { maxAge: 31536000, includeSubDomains: true, preload: false, description: "サブドメインカバー" },
    { maxAge: 31536000, includeSubDomains: true, preload: true, description: "Preload提出" },
  ];

  getCurrentHeader(): string {
    const stage = this.stages[this.currentStage - 1];
    const parts = [`max-age=${stage.maxAge}`];
    if (stage.includeSubDomains) parts.push("includeSubDomains");
    if (stage.preload) parts.push("preload");
    return parts.join("; ");
  }

  promote(): { success: boolean; message: string } {
    if (this.currentStage >= this.stages.length) {
      return { success: false, message: "既に最高レベルです" };
    }
    this.currentStage++;
    const stage = this.stages[this.currentStage - 1];
    return { success: true, message: `Stage ${this.currentStage}に昇格: ${stage.description}` };
  }

  getDeploymentStatus(): object {
    const stage = this.stages[this.currentStage - 1];
    return {
      currentStage: this.currentStage,
      totalStages: this.stages.length,
      header: this.getCurrentHeader(),
      description: stage.description,
      nextStep: this.currentStage < this.stages.length
        ? this.stages[this.currentStage].description
        : "全段階完了",
    };
  }
}

const hstsManager = new HSTSDeploymentManager();
console.log(hstsManager.getCurrentHeader());
console.log(hstsManager.promote());
console.log(hstsManager.getDeploymentStatus());

HTTPからHTTPSへのリダイレクト + HSTS

server {
    listen 80;
    server_name example.com *.example.com;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

パターン3:CORS設定 — Preflight、認証情報、ワイルドカード管理

CORSはセキュリティ脆弱性ではなく、ブラウザのクロスオリジンアクセス制御メカニズムです。設定ミスはAPIへのアクセス不能やセキュリティ境界の破壊を招きます。

CORSコア概念

概念 説明
シンプルリクエスト GET/HEAD/POST、特定のContent-Typeのみ、preflightなし
プレフライトリクエスト ブラウザが先にOPTIONSリクエストを送信し、クロスオリジン許可を確認
認証情報リクエスト Cookieを伴うクロスオリジンリクエスト、Access-Control-Allow-Origin*不可
ワイルドカード *は任意のオリジンにマッチするが、credentials: trueと併用不可

プロダクション級CORSミドルウェア

import { Request, Response, NextFunction } from "express";

interface CORSConfig {
  allowedOrigins: string[];
  allowedMethods: string[];
  allowedHeaders: string[];
  exposedHeaders: string[];
  allowCredentials: boolean;
  maxAge: number;
}

class CORSMiddleware {
  private config: CORSConfig;

  constructor(config: Partial<CORSConfig> = {}) {
    this.config = {
      allowedOrigins: config.allowedOrigins || ["https://example.com"],
      allowedMethods: config.allowedMethods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
      allowedHeaders: config.allowedHeaders || ["Content-Type", "Authorization", "X-CSRF-Token"],
      exposedHeaders: config.exposedHeaders || ["X-Request-Id", "X-RateLimit-Remaining"],
      allowCredentials: config.allowCredentials ?? true,
      maxAge: config.maxAge || 86400,
    };
  }

  handle = (req: Request, res: Response, next: NextFunction): void => {
    const origin = req.headers.origin as string;

    if (!origin) {
      next();
      return;
    }

    const isAllowed = this.config.allowedOrigins.some((allowed) => {
      if (allowed.startsWith("*.") && origin.includes(allowed.slice(1))) {
        return true;
      }
      return allowed === origin;
    });

    if (!isAllowed) {
      res.status(403).json({ error: "CORS origin not allowed" });
      return;
    }

    res.setHeader("Access-Control-Allow-Origin", origin);

    if (this.config.allowCredentials) {
      res.setHeader("Access-Control-Allow-Credentials", "true");
    }

    res.setHeader(
      "Access-Control-Allow-Methods",
      this.config.allowedMethods.join(", ")
    );

    res.setHeader(
      "Access-Control-Allow-Headers",
      this.config.allowedHeaders.join(", ")
    );

    res.setHeader(
      "Access-Control-Expose-Headers",
      this.config.exposedHeaders.join(", ")
    );

    res.setHeader("Access-Control-Max-Age", this.config.maxAge.toString());

    if (req.method === "OPTIONS") {
      res.status(204).end();
      return;
    }

    next();
  };
}

const corsMiddleware = new CORSMiddleware({
  allowedOrigins: [
    "https://example.com",
    "https://app.example.com",
    "*.example.dev",
  ],
  allowedMethods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
  allowCredentials: true,
  maxAge: 86400,
});

app.use(corsMiddleware.handle);

CORS一般的なエラートラブルシューティング

class CORSTroubleshooter {
  diagnose(req: Request, res: Response): string[] {
    const issues: string[] = [];
    const origin = req.headers.origin as string;
    const acaoHeader = res.getHeader("Access-Control-Allow-Origin") as string;
    const acacHeader = res.getHeader("Access-Control-Allow-Credentials") as string;

    if (!origin) {
      issues.push("Origin ヘッダー欠落:ブラウザ以外のリクエストまたは同一オリジンリクエスト、CORSは適用外");
    }

    if (acaoHeader === "*" && acacHeader === "true") {
      issues.push("致命的エラー:Allow-Originが*の場合、Allow-Credentialsをtrueに設定できません");
    }

    if (req.method === "OPTIONS" && res.statusCode !== 204 && res.statusCode !== 200) {
      issues.push(`Preflightリクエストが${res.statusCode}を返しました。204または200であるべきです`);
    }

    const requestHeaders = req.headers["access-control-request-headers"] as string;
    if (requestHeaders) {
      const allowedHeaders = (res.getHeader("Access-Control-Allow-Headers") as string || "").split(", ");
      const requested = requestHeaders.split(", ");
      const missing = requested.filter((h) => !allowedHeaders.includes(h.toLowerCase()));
      if (missing.length > 0) {
        issues.push(`リクエストヘッダー${missing.join(", ")}がAllow-Headersで許可されていません`);
      }
    }

    return issues;
  }
}

パターン4:Permissions-Policyと機能制御 — 位置情報、カメラ、マイク

Permissions-Policy(旧Feature-Policy)は、ページが使用できるブラウザ機能を制御します。XSS攻撃が成功しても、攻撃者は無効化されたAPIを呼び出せません。

基本ディレクティブ設定

Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://map.example.com"), payment=(self), usb=(), magnetometer=(), gyroscope=(), fullscreen=(self), sync-xhr=(self), document-domain=()
ディレクティブ 意味 推奨値 説明
camera カメラ () 無効 ビデオ会議シーン以外
microphone マイク () 無効 音声入力シーン以外
geolocation 位置情報 (self) 同一オリジンのみ マップアプリは緩和可
payment 決済API (self) 同一オリジンのみ ECシーンで必要
usb WebUSB () 無効 ほぼ不要
magnetometer 磁力計 () 無効 ほぼ不要
gyroscope ジャイロスコープ () 無効 ゲームシーンは緩和可
fullscreen フルスクリーン (self) 動画再生に必要
sync-xhr 同期XHR () 無効 パフォーマンスキラー、無効推奨
document-domain document.domain () 無効 ドメイン緩和攻撃を防止

ページごとのきめ細かい設定

import { Request, Response, NextFunction } from "express";

interface PagePermissionPolicy {
  path: string;
  policy: Record<string, string>;
}

class PermissionsPolicyManager {
  private globalPolicy: Record<string, string> = {
    camera: "()",
    microphone: "()",
    geolocation: "(self)",
    payment: "(self)",
    usb: "()",
    magnetometer: "()",
    gyroscope: "()",
    fullscreen: "(self)",
    sync_xhr: "()",
    document_domain: "()",
  };

  private pageOverrides: PagePermissionPolicy[] = [
    {
      path: "/video-call",
      policy: { camera: "(self)", microphone: "(self)" },
    },
    {
      path: "/map",
      policy: { geolocation: "(self https://map.example.com)" },
    },
    {
      path: "/checkout",
      policy: { payment: "(self https://pay.example.com)" },
    },
  ];

  getPolicyHeader(path: string): string {
    let effectivePolicy = { ...this.globalPolicy };

    for (const override of this.pageOverrides) {
      if (path.startsWith(override.path)) {
        effectivePolicy = { ...effectivePolicy, ...override.policy };
      }
    }

    return Object.entries(effectivePolicy)
      .map(([key, value]) => `${key}=${value}`)
      .join(", ");
  }

  middleware = (req: Request, res: Response, next: NextFunction): void => {
    const policyHeader = this.getPolicyHeader(req.path);
    res.setHeader("Permissions-Policy", policyHeader);
    next();
  };
}

const permissionsPolicy = new PermissionsPolicyManager();
app.use(permissionsPolicy.middleware);

フロントエンド機能権限検出

async function checkPermission(featureName) {
  try {
    const result = await navigator.permissions.query({ name: featureName });
    return {
      feature: featureName,
      state: result.state,
      allowed: result.state !== "denied",
    };
  } catch {
    return {
      feature: featureName,
      state: "unsupported",
      allowed: false,
    };
  }
}

async function auditPagePermissions() {
  const features = [
    "camera",
    "microphone",
    "geolocation",
    "notifications",
    "persistent-storage",
  ];
  const results = await Promise.all(features.map(checkPermission));
  return results;
}

auditPagePermissions().then(console.table);

パターン5:プロダクション級セキュリティヘッダー管理 — Nginx/Caddy設定、自動テスト、監視

すべてのセキュリティヘッダーを統合管理し、すべてのエントリポイントで一貫した適用を確保し、自動テストと監視システムを構築します。

完全セキュリティヘッダーチェックリスト

interface SecurityHeadersConfig {
  contentSecurityPolicy: string;
  strictTransportSecurity: string;
  permissionsPolicy: string;
  referrerPolicy: string;
  xContentTypeOptions: string;
  xFrameOptions: string;
  xXSSProtection: string;
  crossOriginOpenerPolicy: string;
  crossOriginEmbedderPolicy: string;
  crossOriginResourcePolicy: string;
}

class SecurityHeadersManager {
  private config: SecurityHeadersConfig;

  constructor(domain: string, apiDomain: string, nonce?: string) {
    const scriptSrc = nonce ? `'self' 'nonce-${nonce}'` : "'self'";

    this.config = {
      contentSecurityPolicy: [
        `default-src 'self'`,
        `script-src ${scriptSrc}`,
        `style-src 'self' 'unsafe-inline'`,
        `img-src 'self' data: https:`,
        `font-src 'self' https://fonts.gstatic.com`,
        `connect-src 'self' ${apiDomain}`,
        `frame-ancestors 'none'`,
        `base-uri 'self'`,
        `form-action 'self'`,
        `upgrade-insecure-requests`,
      ].join("; "),

      strictTransportSecurity: "max-age=31536000; includeSubDomains; preload",

      permissionsPolicy: [
        "camera=()",
        "microphone=()",
        "geolocation=(self)",
        "payment=(self)",
        "usb=()",
        "fullscreen=(self)",
        "sync-xhr=()",
      ].join(", "),

      referrerPolicy: "strict-origin-when-cross-origin",
      xContentTypeOptions: "nosniff",
      xFrameOptions: "DENY",
      xXSSProtection: "0",
      crossOriginOpenerPolicy: "same-origin",
      crossOriginEmbedderPolicy: "require-corp",
      crossOriginResourcePolicy: "same-origin",
    };
  }

  applyHeaders(res: any): void {
    res.setHeader("Content-Security-Policy", this.config.contentSecurityPolicy);
    res.setHeader("Strict-Transport-Security", this.config.strictTransportSecurity);
    res.setHeader("Permissions-Policy", this.config.permissionsPolicy);
    res.setHeader("Referrer-Policy", this.config.referrerPolicy);
    res.setHeader("X-Content-Type-Options", this.config.xContentTypeOptions);
    res.setHeader("X-Frame-Options", this.config.xFrameOptions);
    res.setHeader("X-XSS-Protection", this.config.xXSSProtection);
    res.setHeader("Cross-Origin-Opener-Policy", this.config.crossOriginOpenerPolicy);
    res.setHeader("Cross-Origin-Embedder-Policy", this.config.crossOriginEmbedderPolicy);
    res.setHeader("Cross-Origin-Resource-Policy", this.config.crossOriginResourcePolicy);
  }

  getNginxConfig(): string {
    return `# Nginx Security Headers Configuration
add_header Content-Security-Policy "${this.config.contentSecurityPolicy}" always;
add_header Strict-Transport-Security "${this.config.strictTransportSecurity}" always;
add_header Permissions-Policy "${this.config.permissionsPolicy}" always;
add_header Referrer-Policy "${this.config.referrerPolicy}" always;
add_header X-Content-Type-Options "${this.config.xContentTypeOptions}" always;
add_header X-Frame-Options "${this.config.xFrameOptions}" always;
add_header X-XSS-Protection "${this.config.xXSSProtection}" always;
add_header Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}" always;
add_header Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}" always;
add_header Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}" always;`;
  }

  getCaddyConfig(): string {
    return `# Caddy Security Headers Configuration
header {
    Content-Security-Policy "${this.config.contentSecurityPolicy}"
    Strict-Transport-Security "${this.config.strictTransportSecurity}"
    Permissions-Policy "${this.config.permissionsPolicy}"
    Referrer-Policy "${this.config.referrerPolicy}"
    X-Content-Type-Options "${this.config.xContentTypeOptions}"
    X-Frame-Options "${this.config.xFrameOptions}"
    X-XSS-Protection "${this.config.xXSSProtection}"
    Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}"
    Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}"
    Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}"
}`;
  }
}

const headersManager = new SecurityHeadersManager(
  "example.com",
  "https://api.example.com"
);
console.log(headersManager.getNginxConfig());

自動セキュリティヘッダーテスト

interface HeaderTestResult {
  header: string;
  present: boolean;
  value: string | null;
  passed: boolean;
  recommendation: string;
}

class SecurityHeadersTester {
  private requiredHeaders: Array<{
    name: string;
    validator: (value: string | null) => { passed: boolean; recommendation: string };
  }> = [
    {
      name: "Content-Security-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "CSPの設定は必須です" };
        if (value.includes("'unsafe-inline'") && !value.includes("'nonce-")) {
          return { passed: false, recommendation: "script-srcでunsafe-inlineを使用せず、nonceを使用してください" };
        }
        if (value.includes("'unsafe-eval'")) {
          return { passed: false, recommendation: "unsafe-evalは使用すべきではありません" };
        }
        return { passed: true, recommendation: "CSP設定は適切です" };
      },
    },
    {
      name: "Strict-Transport-Security",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "HSTSの設定は必須です" };
        const maxAge = parseInt(value.match(/max-age=(\d+)/)?.[1] || "0");
        if (maxAge < 2592000) {
          return { passed: false, recommendation: `max-ageは2592000(30日)以上にすべきです。現在: ${maxAge}` };
        }
        return { passed: true, recommendation: "HSTS設定は適切です" };
      },
    },
    {
      name: "X-Content-Type-Options",
      validator: (value) => {
        if (value !== "nosniff") {
          return { passed: false, recommendation: "nosniffに設定すべきです" };
        }
        return { passed: true, recommendation: "正しく設定されています" };
      },
    },
    {
      name: "X-Frame-Options",
      validator: (value) => {
        if (value !== "DENY" && value !== "SAMEORIGIN") {
          return { passed: false, recommendation: "DENYまたはSAMEORIGINに設定すべきです" };
        }
        return { passed: true, recommendation: "正しく設定されています" };
      },
    },
    {
      name: "Referrer-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "Referrer-Policyの設定は必須です" };
        const safeValues = [
          "no-referrer",
          "strict-origin",
          "strict-origin-when-cross-origin",
        ];
        if (!safeValues.includes(value)) {
          return { passed: false, recommendation: `strict-origin-when-cross-originを推奨。現在: ${value}` };
        }
        return { passed: true, recommendation: "正しく設定されています" };
      },
    },
    {
      name: "Permissions-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "Permissions-Policyを設定すべきです" };
        return { passed: true, recommendation: "設定済み" };
      },
    },
  ];

  async testUrl(url: string): Promise<HeaderTestResult[]> {
    const response = await fetch(url);
    const headers = response.headers;

    return this.requiredHeaders.map(({ name, validator }) => {
      const value = headers.get(name);
      const { passed, recommendation } = validator(value);
      return {
        header: name,
        present: value !== null,
        value,
        passed,
        recommendation,
      };
    });
  }

  generateReport(results: HeaderTestResult[]): string {
    const passed = results.filter((r) => r.passed).length;
    const total = results.length;
    const score = Math.round((passed / total) * 100);

    let report = `セキュリティヘッダースコア: ${score}/100 (${passed}/${total} 合格)\n\n`;

    for (const result of results) {
      const icon = result.passed ? "✅" : "❌";
      report += `${icon} ${result.header}\n`;
      report += `   値: ${result.value || "(未設定)"}\n`;
      report += `   推奨: ${result.recommendation}\n\n`;
    }

    return report;
  }
}

const tester = new SecurityHeadersTester();
tester.testUrl("https://example.com").then((results) => {
  console.log(tester.generateReport(results));
});

CI/CD統合 — デプロイごとの自動検出

name: Security Headers Check

on:
  deployment_status:

jobs:
  check-headers:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm install

      - name: Run security headers test
        env:
          TARGET_URL: ${{ github.event.deployment_status.environment_url }}
        run: npx tsx scripts/check-security-headers.ts $TARGET_URL

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: security-headers-report
          path: security-headers-report.json

監視とアラート — セキュリティヘッダー変更検出

interface HeaderSnapshot {
  url: string;
  timestamp: string;
  headers: Record<string, string>;
}

class SecurityHeadersMonitor {
  private snapshots: Map<string, HeaderSnapshot[]> = new Map();
  private alertCallback: (message: string) => void;

  constructor(alertCallback: (message: string) => void) {
    this.alertCallback = alertCallback;
  }

  async check(url: string): Promise<void> {
    const response = await fetch(url);
    const currentHeaders: Record<string, string> = {};

    const securityHeaderNames = [
      "content-security-policy",
      "strict-transport-security",
      "permissions-policy",
      "referrer-policy",
      "x-content-type-options",
      "x-frame-options",
      "cross-origin-opener-policy",
    ];

    for (const name of securityHeaderNames) {
      const value = response.headers.get(name);
      if (value) currentHeaders[name] = value;
    }

    const snapshot: HeaderSnapshot = {
      url,
      timestamp: new Date().toISOString(),
      headers: currentHeaders,
    };

    const previous = this.snapshots.get(url);
    if (previous && previous.length > 0) {
      const lastSnapshot = previous[previous.length - 1];
      this._detectChanges(url, lastSnapshot, snapshot);
    }

    if (!this.snapshots.has(url)) {
      this.snapshots.set(url, []);
    }
    this.snapshots.get(url)!.push(snapshot);
  }

  private _detectChanges(url: string, previous: HeaderSnapshot, current: HeaderSnapshot): void {
    const allKeys = new Set([
      ...Object.keys(previous.headers),
      ...Object.keys(current.headers),
    ]);

    for (const key of allKeys) {
      const prevValue = previous.headers[key];
      const currValue = current.headers[key];

      if (prevValue && !currValue) {
        this.alertCallback(`[ALERT] ${url}: セキュリティヘッダー ${key} が削除されました!`);
      } else if (!prevValue && currValue) {
        this.alertCallback(`[INFO] ${url}: 新しいセキュリティヘッダー ${key} が追加されました`);
      } else if (prevValue !== currValue) {
        if (this._isWeakened(key, prevValue!, currValue!)) {
          this.alertCallback(`[WARNING] ${url}: セキュリティヘッダー ${key} が弱化されました!\n  旧: ${prevValue}\n  新: ${currValue}`);
        }
      }
    }
  }

  private _isWeakened(headerName: string, oldValue: string, newValue: string): boolean {
    if (headerName === "strict-transport-security") {
      const oldMaxAge = parseInt(oldValue.match(/max-age=(\d+)/)?.[1] || "0");
      const newMaxAge = parseInt(newValue.match(/max-age=(\d+)/)?.[1] || "0");
      return newMaxAge < oldMaxAge;
    }

    if (headerName === "content-security-policy") {
      if (!oldValue.includes("'unsafe-inline'") && newValue.includes("'unsafe-inline'")) {
        return true;
      }
      if (!oldValue.includes("'unsafe-eval'") && newValue.includes("'unsafe-eval'")) {
        return true;
      }
    }

    return false;
  }
}

const monitor = new SecurityHeadersMonitor((msg) => console.log(msg));

setInterval(() => {
  monitor.check("https://example.com");
}, 300000);

5つのよくある落とし穴

落とし穴1:CSPでの'unsafe-inline'の使用

❌ Content-Security-Policy: script-src 'self' 'unsafe-inline'
✅ Content-Security-Policy: script-src 'self' 'nonce-a1b2c3d4'

'unsafe-inline'はすべてのインラインスクリプトの実行を許可し、CSPのXSS防御を完全に無効にします。nonceまたはhashを使用してください。

落とし穴2:HSTSでいきなりmax-age=31536000を設定

❌ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
✅ Strict-Transport-Security: max-age=300  (まず5分でテスト)

1年を直接設定すると、HTTPSに問題が発生した場合、ユーザーは数週間アクセスできなくなります。短い期間から開始し、段階的に増やす必要があります。

落とし穴3:CORSでAccess-Control-Allow-Origin: *と認証情報の組み合わせ

❌ Access-Control-Allow-Origin: *
❌ Access-Control-Allow-Credentials: true

ブラウザ仕様はこの組み合わせを明確に禁止しています。認証情報を伴う場合、具体的なOriginを指定する必要があり、ワイルドカードは使用できません。

落とし穴4:Nginxのadd_headeralwaysを使用しない

❌ add_header X-Frame-Options DENY;
✅ add_header X-Frame-Options DENY always;

alwaysがない場合、レスポンスステータスが404/500のときにセキュリティヘッダーが追加されません。攻撃者はエラーページを利用して保護をバイパスできます。

落とし穴5:Cross-Origin分離ヘッダーの無視

❌ Cross-Origin-Opener-Policy と Cross-Origin-Embedder-Policy が欠落
✅ Cross-Origin-Opener-Policy: same-origin
✅ Cross-Origin-Embedder-Policy: require-corp

COOP/COEPヘッダーがないと、ページはSharedArrayBufferなどの高度なAPIを使用できず、Spectreクラスのサイドチャネル攻撃に対して脆弱になります。


トラブルシューティング表

症状 可能な原因 トラブルシューティング手順 解決策
インラインスクリプトが実行されない CSPにnonceが不足 script-srcにnonce値が含まれているか確認 リクエストごとに一意のnonceを生成して注入
CDNリソースの読み込みに失敗 CSPがCDNドメインを許可していない ブラウザコンソールのCSPエラーを確認 該当ディレクティブにCDNドメインを追加
HTTPSページがHTTPでもアクセス可能 HSTSが有効でない HTTPSレスポンスでHSTSヘッダーが返されているか確認 HSTSはHTTPSレスポンスでのみ有効
CORSプレフライトが失敗 OPTIONSが正しく処理されていない サーバーがOPTIONSメソッドを処理しているか確認 204を返し、正しいCORSヘッダーを設定
credentials: trueでエラー Allow-Originが* CORSレスポンスヘッダーの組み合わせを確認 具体的なOrigin値に変更
APIクロスオリジンリクエストが拒否 Access-Control-Allow-Headersが不足 リクエストヘッダーがAllow-Headersに含まれているか確認 カスタムリクエストヘッダーをホワイトリストに追加
Geolocation APIが利用不可 Permissions-Policyで無効化 geolocationディレクティブの設定を確認 必要なページでgeolocation権限を緩和
SharedArrayBufferが利用不可 COOP/COEPヘッダーが不足 Cross-Origin分離ヘッダーを確認 COOP: same-originとCOEP: require-corpを追加
エラーページでセキュリティヘッダーが欠落 Nginx add_headerにalwaysが不足 404/500ページのレスポンスヘッダーを確認 alwaysパラメータを追加
サブドメインがHTTPでアクセス可能 HSTSにincludeSubDomainsが未設定 HSTSディレクティブを確認 全サブドメインのHTTPS対応を確認後に追加

高度な最適化

最適化1:CSPレポートの収集と分析

CSP違反レポートを一元収集し、セキュリティ状況認識を構築します。

import express from "express";

interface CSPReportPayload {
  "csp-report": {
    "document-uri": string;
    referrer: string;
    "violated-directive": string;
    "effective-directive": string;
    "original-policy": string;
    disposition: string;
    "blocked-uri": string;
    "line-number": number;
    "column-number": number;
    "source-file": string;
    "status-code": number;
  };
}

class CSPReportCollector {
  private reports: Array<{
    timestamp: string;
    blockedUri: string;
    violatedDirective: string;
    documentUri: string;
    source: string;
  }> = [];

  handleReport = (req: express.Request, res: express.Response): void => {
    const payload = req.body as CSPReportPayload;
    const report = payload["csp-report"];

    this.reports.push({
      timestamp: new Date().toISOString(),
      blockedUri: report["blocked-uri"],
      violatedDirective: report["violated-directive"],
      documentUri: report["document-uri"],
      source: report["source-file"] || "unknown",
    });

    if (this.reports.length % 100 === 0) {
      this._analyze();
    }

    res.status(204).end();
  };

  private _analyze(): void {
    const directiveCounts: Record<string, number> = {};
    const blockedUriCounts: Record<string, number> = {};

    for (const report of this.reports) {
      directiveCounts[report.violatedDirective] =
        (directiveCounts[report.violatedDirective] || 0) + 1;
      blockedUriCounts[report.blockedUri] =
        (blockedUriCounts[report.blockedUri] || 0) + 1;
    }

    console.log("[CSP Analysis] 違反の多いディレクティブ:", directiveCounts);
    console.log("[CSP Analysis] ブロックの多いURI:", blockedUriCounts);
  }

  getTopViolations(limit: number = 10): Array<{
    directive: string;
    blockedUri: string;
    count: number;
  }> {
    const counts: Record<string, number> = {};
    for (const report of this.reports) {
      const key = `${report.violatedDirective}|${report.blockedUri}`;
      counts[key] = (counts[key] || 0) + 1;
    }

    return Object.entries(counts)
      .sort(([, a], [, b]) => b - a)
      .slice(0, limit)
      .map(([key, count]) => {
        const [directive, blockedUri] = key.split("|");
        return { directive, blockedUri, count };
      });
  }
}

const cspCollector = new CSPReportCollector();
const reportApp = express();
reportApp.use(express.json({ type: ["application/json", "application/csp-report"] }));
reportApp.post("/api/csp-report", cspCollector.handleReport);

最適化2:動的CSP — ページタイプに応じた異なるポリシー生成

interface PageCSPProfile {
  scripts: string[];
  styles: string[];
  images: string[];
  connect: string[];
  frame: string[];
}

class DynamicCSPGenerator {
  private profiles: Map<string, PageCSPProfile> = new Map([
    [
      "homepage",
      {
        scripts: ["'self'", "https://cdn.example.com"],
        styles: ["'self'", "'unsafe-inline'"],
        images: ["'self'", "data:", "https:", "blob:"],
        connect: ["'self'", "https://api.example.com"],
        frame: ["'none'"],
      },
    ],
    [
      "dashboard",
      {
        scripts: ["'self'", "'nonce-{{NONCE}}'", "https://cdn.example.com", "https://charts.example.com"],
        styles: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
        images: ["'self'", "data:", "https:", "blob:"],
        connect: ["'self'", "https://api.example.com", "wss://ws.example.com"],
        frame: ["'self'", "https://embed.example.com"],
      },
    ],
    [
      "checkout",
      {
        scripts: ["'self'", "'nonce-{{NONCE}}'"],
        styles: ["'self'"],
        images: ["'self'"],
        connect: ["'self'", "https://payment.example.com"],
        frame: ["https://payment.example.com"],
      },
    ],
  ]);

  generate(profileName: string, nonce: string): string {
    const profile = this.profiles.get(profileName);
    if (!profile) throw new Error(`Unknown profile: ${profileName}`);

    const directives: string[] = [];

    directives.push(`default-src 'self'`);
    directives.push(`script-src ${profile.scripts.map((s) => s.replace("{{NONCE}}", nonce)).join(" ")}`);
    directives.push(`style-src ${profile.styles.join(" ")}`);
    directives.push(`img-src ${profile.images.join(" ")}`);
    directives.push(`connect-src ${profile.connect.join(" ")}`);
    directives.push(`frame-ancestors ${profile.frame.join(" ")}`);
    directives.push(`base-uri 'self'`);
    directives.push(`form-action 'self'`);

    return directives.join("; ");
  }
}

const cspGenerator = new DynamicCSPGenerator();
const homepageCSP = cspGenerator.generate("homepage", "placeholder");
const checkoutCSP = cspGenerator.generate("checkout", "a1b2c3d4");
console.log("Homepage CSP:", homepageCSP);
console.log("Checkout CSP:", checkoutCSP);

最適化3:Expect-CTと証明書透明性

Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"

ChromeはExpect-CTのサポートを削除しましたが(2026年)、Certificate TransparencyはHTTPSセキュリティの重要な保障として残っています。証明書がCTログを通じて発行されていることを確認してください:

class CTLogVerifier {
  verifyCertificate(pemCertificate: string): {
    ctCompliant: boolean;
    scts: Array<{ logId: string; timestamp: number; signature: string }>;
  } {
    return {
      ctCompliant: true,
      scts: [
        { logId: "google-argon-2026", timestamp: Date.now(), signature: "verified" },
        { logId: "cloudflare-nimbus-2026", timestamp: Date.now(), signature: "verified" },
      ],
    };
  }
}

ツール比較

機能 Mozilla Observatory SecurityHeaders.com HSTS Preload CSP Evaluator
検出範囲 全セキュリティヘッダー 全セキュリティヘッダー HSTS Preload状態 CSPポリシー分析
スコアリング A-Fグレード A-Fグレード 合格/不合格 セキュリティ推奨
CSP深い分析 基本 基本 該当なし ディレクティブ深い分析
HSTS Preloadチェック ✅ 専用
APIサポート
CI/CD統合
無料
適用シーン 包括的スキャン クイックチェック Preload提出 CSPポリシー最適化

推奨:プロダクション環境ではMozilla Observatoryで包括的スキャン + CSP Evaluatorでポリシー最適化 + HSTS Preloadで提出を行ってください。


まとめ

セキュリティヘッダーはWebセキュリティにおける「コストパフォーマンスの王様」です。ビジネスコードを変更することなく、わずか数行のサーバー設定でXSS、クリックジャッキング、プロトコルダウングレード、MIMEスニッフィングなど多くの攻撃を防御できます。CSPはXSSに対する究極の武器、HSTSはHTTPSの守護者、CORSはクロスオリジンアクセスの門番、Permissions-Policyはブラウザ機能のスイッチ、そして統合管理と自動テストはこれらのヘッダーがプロダクション環境で継続的に有効であることを保証する鍵です。


推奨ツール

ブラウザローカルツールを無料で試す →

#Web安全头#CSP#HSTS#CORS#安全响应头#2026#前端安全