← ブログ一覧

セキュリティヘッダーと CSP の設定方法:実務実装ガイド

XSS・クリックジャッキング・情報漏洩を防ぐセキュリティヘッダーの設定方法を解説。CSP・HSTS・X-Frame-Options の実装パターン、Next.js/Express での設定例、段階的導入手順まで網羅した実務ガイド。

#セキュリティ#Web#Next.js#Node.js
セキュリティヘッダーと CSP の設定方法:実務実装ガイド

セキュリティヘッダーと CSP の設定方法:実務実装ガイド

Web アプリケーションのセキュリティ対策として、セキュリティヘッダーの設定は最も費用対効果が高い施策の一つです。適切に設定すれば、XSS(クロスサイトスクリプティング)、クリックジャッキング、情報漏洩などの脅威を大幅に軽減できます。

本記事では、受託開発・自社開発の現場で即使えるセキュリティヘッダーの実装方法を、具体的なコード例・段階的導入手順・トラブルシューティングを含めて解説します。


1. 設定すべきセキュリティヘッダーの優先順位

必須レベル(全プロジェクトで設定すべき)

| ヘッダー名 | 目的 | 推奨値 | |---|---|---| | Content-Security-Policy | XSS 攻撃の防止 | default-src 'self' から始める | | X-Content-Type-Options | MIME タイプスニッフィング防止 | nosniff | | X-Frame-Options | クリックジャッキング防止 | DENY または SAMEORIGIN | | Strict-Transport-Security | HTTP → HTTPS 強制 | max-age=31536000; includeSubDomains |

推奨レベル(可能な限り設定)

| ヘッダー名 | 目的 | 推奨値 | |---|---|---| | Referrer-Policy | リファラー情報の制御 | strict-origin-when-cross-origin | | Permissions-Policy | ブラウザ機能の制限 | camera=(), microphone=(), geolocation=() | | X-XSS-Protection | 古いブラウザの XSS 保護(非推奨化傾向) | 1; mode=block |

実務判断ポイント

  • CSP は段階的に厳格化:初期は report-only モードで運用し、エラーを確認してから本番適用
  • HSTS は証明書の準備が整ってから:一度設定すると HTTP に戻せない
  • X-Frame-Options vs CSP の frame-ancestors:両方設定するのがベストプラクティス

2. Content Security Policy(CSP)の基本設定

CSP のディレクティブ一覧

// CSP ディレクティブの構造
interface CSPDirectives {
  'default-src': string[];      // デフォルトのリソース読み込み元
  'script-src': string[];       // JavaScript の読み込み元
  'style-src': string[];        // CSS の読み込み元
  'img-src': string[];          // 画像の読み込み元
  'connect-src': string[];      // fetch/XHR の接続先
  'font-src': string[];         // フォントの読み込み元
  'object-src': string[];       // <object>, <embed> の読み込み元
  'media-src': string[];        // <audio>, <video> の読み込み元
  'frame-src': string[];        // <iframe> の読み込み元
  'frame-ancestors': string[];  // このページを埋め込める親ページ
  'base-uri': string[];         // <base> タグの制限
  'form-action': string[];      // <form> の送信先
}

段階的な CSP 設定例

レベル 1:最も厳格な設定(推奨出発点)

// Next.js の next.config.js
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
`;

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
          },
        ],
      },
    ];
  },
};

レベル 2:外部サービス利用時の設定

// Google Analytics, Vercel Analytics などを使う場合
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://va.vercel-scripts.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https: blob:;
  font-src 'self' data:;
  connect-src 'self' https://www.google-analytics.com https://vitals.vercel-insights.com;
  frame-ancestors 'none';
`;

CSP の段階的導入手順

ステップ 1:Report-Only モードで検証

// 本番適用前に報告のみ行う
headers: [
  {
    key: 'Content-Security-Policy-Report-Only',
    value: ContentSecurityPolicy + ` report-uri /api/csp-report;`,
  },
];

ステップ 2:CSP レポートの収集

// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const report = await request.json();
  
  console.error('CSP Violation:', {
    blockedURI: report['csp-report']['blocked-uri'],
    violatedDirective: report['csp-report']['violated-directive'],
    documentURI: report['csp-report']['document-uri'],
  });
  
  // 本番環境では Sentry や DataDog に送信
  // await sendToMonitoring(report);
  
  return NextResponse.json({ received: true });
}

ステップ 3:エラーを修正して本番適用

// Report-Only を外して本番適用
headers: [
  {
    key: 'Content-Security-Policy',
    value: ContentSecurityPolicy,
  },
];

3. Next.js での実装パターン

パターン A:next.config.js で一括設定

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline';",
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
};

パターン B:Middleware で動的設定

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 管理画面と公開ページで異なる CSP を適用
  const isAdminPath = request.nextUrl.pathname.startsWith('/admin');
  
  const csp = isAdminPath
    ? "default-src 'self'; frame-ancestors 'none';" // 管理画面:厳格
    : "default-src 'self'; img-src 'self' https:;";  // 公開ページ:緩和
  
  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');
  
  return response;
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

パターン C:環境別の設定

// lib/security-headers.ts
const isDevelopment = process.env.NODE_ENV === 'development';

export const getSecurityHeaders = () => {
  const csp = isDevelopment
    ? "default-src 'self' 'unsafe-eval' 'unsafe-inline';" // 開発環境:緩和
    : "default-src 'self';"; // 本番環境:厳格
  
  return [
    { key: 'Content-Security-Policy', value: csp },
    { key: 'X-Content-Type-Options', value: 'nosniff' },
    { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  ];
};

// next.config.js
const { getSecurityHeaders } = require('./lib/security-headers');

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: getSecurityHeaders(),
      },
    ];
  },
};

4. Express(Node.js)での実装パターン

Helmet を使った基本設定

// server.ts
import express from 'express';
import helmet from 'helmet';

const app = express();

// Helmet で主要なセキュリティヘッダーを一括設定
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'https:'],
        connectSrc: ["'self'"],
        fontSrc: ["'self'"],
        objectSrc: ["'none'"],
        frameAncestors: ["'none'"],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
    referrerPolicy: {
      policy: 'strict-origin-when-cross-origin',
    },
  })
);

app.listen(3000);

CSP Nonce の動的生成(推奨)

import crypto from 'crypto';
import helmet from 'helmet';

app.use((req, res, next) => {
  // リクエストごとに一意の nonce を生成
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: [
          "'self'",
          (req, res) => `'nonce-${res.locals.cspNonce}'`,
        ],
      },
    },
  })
);

// テンプレートで nonce を使用
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <script nonce="${res.locals.cspNonce}">
          console.log('This script is allowed by CSP');
        </script>
      </head>
      <body>Hello</body>
    </html>
  `);
});

5. よくある問題とトラブルシューティング

問題 1:inline script / style が動かない

症状

<!-- この script が CSP でブロックされる -->
<script>
  console.log('Hello');
</script>

解決策 A:nonce を使う(推奨)

// CSP 設定
script-src 'self' 'nonce-{random}';

// HTML
<script nonce="{server-generated-nonce}">
  console.log('Hello');
</script>

解決策 B:hash を使う

// スクリプトの SHA-256 ハッシュを計算
const scriptContent = "console.log('Hello');";
const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');

// CSP 設定
script-src 'self' 'sha256-{hash}';

解決策 C:unsafe-inline(非推奨)

// 最終手段:セキュリティが低下
script-src 'self' 'unsafe-inline';

問題 2:Google Analytics が動かない

CSP 設定の修正

const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com;
  connect-src 'self' https://www.google-analytics.com;
  img-src 'self' https://www.google-analytics.com;
`;

問題 3:画像が表示されない

チェックリスト

// ✅ data: URI スキームを許可
img-src 'self' data:;

// ✅ 外部 CDN からの画像
img-src 'self' https://cdn.example.com;

// ✅ すべての HTTPS 画像(開発時のみ)
img-src 'self' https:;

問題 4:iframe が表示されない

// YouTube 埋め込みの場合
frame-src 'self' https://www.youtube.com;

// すべての iframe を禁止したい場合
frame-src 'none';

6. セキュリティヘッダーのテスト方法

ブラウザの開発者ツールでチェック

# Chrome DevTools
1. Network タブを開く
2. ページをリロード
3. ドキュメント(最初のリクエスト)を選択
4. Headers タブで Response Headers を確認

curl コマンドでチェック

# セキュリティヘッダーを確認
curl -I https://example.com

# 特定のヘッダーのみ抽出
curl -I https://example.com | grep -i "content-security-policy"

オンラインツールでスキャン

// 推奨ツール一覧
const securityScanners = [
  {
    name: 'Security Headers',
    url: 'https://securityheaders.com/',
    features: ['A+ 評価', '改善提案'],
  },
  {
    name: 'Mozilla Observatory',
    url: 'https://observatory.mozilla.org/',
    features: ['詳細スコア', 'ベストプラクティス'],
  },
  {
    name: 'CSP Evaluator',
    url: 'https://csp-evaluator.withgoogle.com/',
    features: ['CSP 専用', 'ポリシー検証'],
  },
];

自動テスト(Playwright)

// tests/security-headers.spec.ts
import { test, expect } from '@playwright/test';

test('セキュリティヘッダーが設定されている', async ({ page }) => {
  const response = await page.goto('https://example.com');
  const headers = response?.headers();
  
  // CSP の確認
  expect(headers?.['content-security-policy']).toBeTruthy();
  expect(headers?.['content-security-policy']).toContain("default-src 'self'");
  
  // その他のヘッダー確認
  expect(headers?.['x-content-type-options']).toBe('nosniff');
  expect(headers?.['x-frame-options']).toBe('DENY');
  expect(headers?.['strict-transport-security']).toContain('max-age=');
});

7. 環境別の設定管理

開発・ステージング・本番の切り替え

// lib/security-config.ts
interface SecurityConfig {
  csp: string;
  hsts: boolean;
  reportOnly: boolean;
}

const configs: Record<string, SecurityConfig> = {
  development: {
    csp: "default-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src 'self' data: https:;",
    hsts: false,
    reportOnly: false,
  },
  staging: {
    csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' https:;",
    hsts: true,
    reportOnly: true, // Report-Only モードで検証
  },
  production: {
    csp: "default-src 'self'; script-src 'self'; img-src 'self' https:;",
    hsts: true,
    reportOnly: false,
  },
};

export const getSecurityConfig = (): SecurityConfig => {
  const env = process.env.NODE_ENV || 'development';
  return configs[env] || configs.development;
};

Vercel での環境変数管理

# .env.local(開発環境)
SECURITY_CSP_MODE=development

# Vercel 環境変数(本番)
SECURITY_CSP_MODE=production
// next.config.js
const cspMode = process.env.SECURITY_CSP_MODE || 'development';

const cspPolicies = {
  development: "default-src 'self' 'unsafe-inline';",
  production: "default-src 'self';",
};

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: cspPolicies[cspMode],
          },
        ],
      },
    ];
  },
};

8. チェックリスト:セキュリティヘッダー導入の流れ

フェーズ 1:現状調査(1〜2日)

  • [ ] 現在のヘッダー設定を curl / DevTools で確認
  • [ ] Security Headers でスコアを測定
  • [ ] 使用中の外部サービス(Analytics, CDN など)をリストアップ

フェーズ 2:CSP 設計(2〜3日)

  • [ ] Report-Only モードで CSP を設定
  • [ ] CSP レポート収集の API エンドポイントを実装
  • [ ] 1 週間運用して違反レポートを確認
  • [ ] 違反の原因を特定し、CSP または実装を修正

フェーズ 3:本番適用(1日)

  • [ ] CSP を Report-Only から本番モードに切り替え
  • [ ] X-Content-Type-Options を設定
  • [ ] X-Frame-Options を設定
  • [ ] Referrer-Policy を設定
  • [ ] Permissions-Policy を設定

フェーズ 4:HSTS 設定(証明書確認後)

  • [ ] HTTPS 証明書が正しく設定されているか確認
  • [ ] サブドメインの HTTPS 対応を確認
  • [ ] HSTS ヘッダーを設定(max-age を短くして開始)
  • [ ] 1 週間後に max-age を長期間(1 年)に変更

フェーズ 5:継続監視

  • [ ] CSP レポートを定期的に確認
  • [ ] Security Headers スコアを月次でチェック
  • [ ] 新規外部サービス導入時に CSP を更新

まとめ

セキュリティヘッダーの設定は、一度設定すれば継続的に効果を発揮する高い投資対効果を持つ施策です。

本記事のポイント

  1. CSP は段階的に:Report-Only → 本番適用の順で安全に導入
  2. Nonce / Hash で inline script を許可unsafe-inline はできる限り避ける
  3. 環境別に設定を切り替え:開発環境では緩和、本番環境では厳格に
  4. HSTS は HTTPS 完全対応後に設定:一度設定すると戻せない
  5. 継続的な監視が重要:CSP レポートを定期的に確認し、ポリシーを改善

Next.js / Express のいずれでも実装可能で、本記事のコード例をそのまま使えば、数時間でセキュリティヘッダーの基本設定が完了します。


Yureate について

Yureate は、スタートアップ・中小企業向けの受託開発会社です。セキュリティ設計を含む Web アプリケーション開発、技術顧問、コードレビューなど、技術面でお困りのことがあればお気軽にご相談ください

この内容について相談する他の記事を見る