セキュリティヘッダーとは?Webサイトを守る必須の HTTP ヘッダー設定

セキュリティヘッダーとは?

セキュリティヘッダーとは、Webサーバーがブラウザに送信するHTTPレスポンスヘッダーの中で、Webサイトのセキュリティを向上させる目的で設定されるヘッダーのことです。

これらのヘッダーを適切に設定することで、以下のような攻撃から Webサイトを保護できます:

  • XSS(クロスサイトスクリプティング)攻撃
  • クリックジャッキング攻撃
  • 中間者攻撃
  • コンテンツタイプ詐称攻撃
  • リファラー情報漏洩
  • HTTPSダウングレード攻撃

セキュリティヘッダーの大きな利点は、サーバーの設定だけで有効化でき、アプリケーションコードを変更する必要がないことです。正しく設定するだけで、Webサイトのセキュリティレベルを大幅に向上させることができます。

主要なセキュリティヘッダー一覧

1. Content-Security-Policy (CSP)

最も重要なセキュリティヘッダーの一つで、XSS攻撃を防ぐために使用されます。

基本的な設定例:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;

段階的なCSP実装:

# ステップ1: レポートのみモード(本番影響なし)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

# ステップ2: 基本的な制限
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

# ステップ3: より厳格な制限
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' https:; connect-src 'self'

より安全な nonce ベースのCSP:

<!-- サーバーサイドで一意のnonceを生成 -->
<script nonce="random-nonce-value">
    // 信頼できるスクリプト
    console.log('This script is allowed');
</script>
Content-Security-Policy: script-src 'nonce-random-nonce-value'

2. X-Frame-Options

クリックジャッキング攻撃を防ぐためのヘッダーです。

# 完全にフレーム埋め込みを禁止
X-Frame-Options: DENY

# 同一オリジンのみ許可
X-Frame-Options: SAMEORIGIN

# 特定のサイトのみ許可(廃止予定)
X-Frame-Options: ALLOW-FROM https://trusted-site.com

現代的なアプローチ(CSP frame-ancestors):

Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors https://trusted-site.com

3. Strict-Transport-Security (HSTS)

HTTPS接続を強制し、HTTPSダウングレード攻撃を防ぎます。

# 基本設定(1年間)
Strict-Transport-Security: max-age=31536000

# サブドメインも含める
Strict-Transport-Security: max-age=31536000; includeSubDomains

# ブラウザのプリロードリストに追加
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

HSTSプリロード申請:

# プリロード申請の条件
1. 有効なSSL証明書
2. HTTPからHTTPSへのリダイレクト
3. preload ディレクティブ付きのHSTSヘッダー
4. https://hstspreload.org/ での申請

4. X-Content-Type-Options

ブラウザのMIMEタイプ推測機能を無効化し、コンテンツタイプ詐称攻撃を防ぎます。

X-Content-Type-Options: nosniff

攻撃例と防御:

<!-- 攻撃者がアップロードした「画像」ファイル -->
<!-- 実際はJavaScriptコードが含まれている -->
<img src="malicious-file.jpg">

<!-- X-Content-Type-Options: nosniff があると -->
<!-- ブラウザはContent-Typeを厳密にチェックし、実行を阻止 -->

5. Referrer-Policy

リファラー情報の送信を制御し、プライバシーを保護します。

# リファラー情報を送信しない
Referrer-Policy: no-referrer

# 同一オリジンの場合のみ送信
Referrer-Policy: same-origin

# HTTPS→HTTPの場合はリファラーを送信しない
Referrer-Policy: no-referrer-when-downgrade

# オリジン部分のみ送信
Referrer-Policy: origin

# 厳格なオリジンポリシー
Referrer-Policy: strict-origin-when-cross-origin

6. Permissions-Policy

ブラウザの機能(カメラ、マイク、位置情報など)へのアクセスを制御します。

# カメラとマイクを完全に無効化
Permissions-Policy: camera=(), microphone=()

# 同一オリジンのみ許可
Permissions-Policy: geolocation=(self)

# 特定のサイトのみ許可
Permissions-Policy: camera=(self "https://trusted-video-service.com")

# 複数の機能を制御
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(self)

7. X-XSS-Protection

古いブラウザ向けのXSS保護機能(現在は非推奨、CSPを使用推奨)。

# XSS保護を有効化(古いブラウザ用)
X-XSS-Protection: 1; mode=block

8. Cache-Control

キャッシュ制御によるセキュリティ向上:

# 機密情報はキャッシュしない
Cache-Control: no-store, no-cache, must-revalidate, private

# 静的リソースは適切にキャッシュ
Cache-Control: public, max-age=31536000, immutable

Webサーバー別の設定方法

Apache (.htaccess)

# セキュリティヘッダーの設定

# CSP
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;"

# X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"

# HSTS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

# X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"

# Referrer Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"

# Permissions Policy
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"

# X-XSS-Protection(古いブラウザ用)
Header always set X-XSS-Protection "1; mode=block"

# HTTPSリダイレクト
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Nginx

# セキュリティヘッダーの設定

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

    # SSL設定
    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;

    # セキュリティヘッダー
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # HTTPからHTTPSへのリダイレクト
    if ($scheme != "https") {
        return 301 https://$server_name$request_uri;
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

IIS (web.config)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <httpProtocol>
            <customHeaders>
                <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;" />
                <add name="X-Frame-Options" value="SAMEORIGIN" />
                <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
                <add name="X-Content-Type-Options" value="nosniff" />
                <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
                <add name="Permissions-Policy" value="geolocation=(), camera=(), microphone=()" />
                <add name="X-XSS-Protection" value="1; mode=block" />
            </customHeaders>
        </httpProtocol>

        <rewrite>
            <rules>
                <rule name="Redirect to HTTPS" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTPS}" pattern="off" ignoreCase="true" />
                    </conditions>
                    <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" 
                            redirectType="Permanent" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

アプリケーション別の実装

Express.js (Node.js)

const express = require('express');
const helmet = require('helmet');

const app = express();

// Helmet を使用した包括的なセキュリティヘッダー設定
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "'unsafe-inline'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            imgSrc: ["'self'", "https:", "data:"],
            connectSrc: ["'self'"],
            fontSrc: ["'self'"],
            objectSrc: ["'none'"],
            mediaSrc: ["'self'"],
            frameSrc: ["'none'"],
        },
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    },
    frameguard: {
        action: 'sameorigin'
    },
    noSniff: true,
    xssFilter: true,
    referrerPolicy: {
        policy: "strict-origin-when-cross-origin"
    },
    permissionsPolicy: {
        geolocation: [],
        camera: [],
        microphone: []
    }
}));

// カスタムヘッダーの追加
app.use((req, res, next) => {
    // 開発環境でのみ緩い設定
    if (process.env.NODE_ENV === 'development') {
        res.setHeader('Content-Security-Policy', 
            "default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'unsafe-inline' 'unsafe-eval';");
    }

    next();
});

app.listen(3000, () => {
    console.log('Server running with security headers');
});

Laravel (PHP)

<?php
// app/Http/Middleware/SecurityHeaders.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // セキュリティヘッダーの設定
        $response->headers->set('Content-Security-Policy', 
            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;");

        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

        $response->headers->set('Strict-Transport-Security', 
            'max-age=31536000; includeSubDomains; preload');

        $response->headers->set('X-Content-Type-Options', 'nosniff');

        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');

        $response->headers->set('Permissions-Policy', 
            'geolocation=(), camera=(), microphone=()');

        $response->headers->set('X-XSS-Protection', '1; mode=block');

        return $response;
    }
}

// app/Http/Kernel.php で登録
protected $middleware = [
    // 他のミドルウェア...
    \App\Http\Middleware\SecurityHeaders::class,
];

// またはconfig/secure-headers.phpを作成
return [
    'content-security-policy' => "default-src 'self'; script-src 'self' 'unsafe-inline';",
    'x-frame-options' => 'SAMEORIGIN',
    'strict-transport-security' => 'max-age=31536000; includeSubDomains; preload',
    'x-content-type-options' => 'nosniff',
    'referrer-policy' => 'strict-origin-when-cross-origin',
    'permissions-policy' => 'geolocation=(), camera=(), microphone=()',
];
?>

Django (Python)

# settings.py
import os

# セキュリティヘッダーの設定
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'SAMEORIGIN'

# HSTS設定
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# SSL設定
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# CSP設定
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "https:", "data:")

# django-csp パッケージを使用
MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    # 他のミドルウェア...
]

# カスタムミドルウェア
# middleware/security_headers.py
class SecurityHeadersMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        # カスタムヘッダーの追加
        response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
        response['Permissions-Policy'] = 'geolocation=(), camera=(), microphone=()'

        return response

セキュリティヘッダーのテストと検証

1. オンラインツールでの検証

# Security Headers チェックサイト

Analyse your HTTP response headers
Quickly and easily assess the security of your HTTP response headers
# Mozilla Observatory
HTTP Header Security Test - HTTP Observatory | MDN
Test your site’s HTTP headers, including CSP and HSTS, to find security problems and get actionable recommendations to m...
# CSP Evaluator
CSP Evaluator
CSP Evaluator

2. ブラウザ開発者ツールでの確認

// ブラウザコンソールでのヘッダー確認
fetch('/')
  .then(response => {
    console.log('CSP:', response.headers.get('Content-Security-Policy'));
    console.log('X-Frame-Options:', response.headers.get('X-Frame-Options'));
    console.log('HSTS:', response.headers.get('Strict-Transport-Security'));
  });

3. curl コマンドでの確認

# ヘッダー情報を確認
curl -I https://example.com

# 特定のヘッダーのみ確認
curl -I https://example.com | grep -i "content-security-policy"
curl -I https://example.com | grep -i "x-frame-options"
curl -I https://example.com | grep -i "strict-transport-security"

# HTTPからHTTPSへのリダイレクト確認
curl -I http://example.com

4. 自動テストの実装

// Jest でのセキュリティヘッダーテスト
describe('Security Headers', () => {
    test('CSP header should be present', async () => {
        const response = await request(app).get('/');
        expect(response.headers['content-security-policy']).toBeDefined();
        expect(response.headers['content-security-policy']).toContain("default-src 'self'");
    });

    test('X-Frame-Options should be SAMEORIGIN', async () => {
        const response = await request(app).get('/');
        expect(response.headers['x-frame-options']).toBe('SAMEORIGIN');
    });

    test('HSTS header should be present', async () => {
        const response = await request(app).get('/');
        expect(response.headers['strict-transport-security']).toContain('max-age=31536000');
    });
});
# Python でのセキュリティヘッダーテスト
import requests
import unittest

class SecurityHeadersTest(unittest.TestCase):
    def setUp(self):
        self.base_url = 'https://example.com'

    def test_csp_header(self):
        response = requests.get(self.base_url)
        self.assertIn('Content-Security-Policy', response.headers)
        self.assertIn("default-src 'self'", response.headers['Content-Security-Policy'])

    def test_x_frame_options(self):
        response = requests.get(self.base_url)
        self.assertEqual(response.headers.get('X-Frame-Options'), 'SAMEORIGIN')

    def test_hsts_header(self):
        response = requests.get(self.base_url)
        hsts = response.headers.get('Strict-Transport-Security')
        self.assertIsNotNone(hsts)
        self.assertIn('max-age=31536000', hsts)

if __name__ == '__main__':
    unittest.main()

よくあるCSPエラーと対処法

1. インラインスクリプトエラー

// エラーが発生するパターン
<script>
    console.log('This will be blocked by CSP');
</script>

// 解決方法1: nonce を使用
<script nonce="abc123">
    console.log('This is allowed with nonce');
</script>

// 解決方法2: 外部ファイルに移動
<script src="/js/app.js"></script>

// 解決方法3: CSPでunsafe-inlineを許可(非推奨)
Content-Security-Policy: script-src 'self' 'unsafe-inline'

2. 外部リソース読み込みエラー

# エラー: Google Fonts が読み込めない
Content-Security-Policy: default-src 'self'

# 解決: font-srcとstyle-srcを追加
Content-Security-Policy: default-src 'self'; font-src https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com

3. Ajax/Fetch エラー

# エラー: API呼び出しが失敗
Content-Security-Policy: default-src 'self'

# 解決: connect-srcを追加
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com

セキュリティヘッダーのベストプラクティス

1. 段階的な実装

# フェーズ1: レポートモードで問題を特定
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

# フェーズ2: 基本的な制限を適用
Content-Security-Policy: default-src 'self' 'unsafe-inline'

# フェーズ3: より厳格な制限
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'

2. 環境別の設定

// 開発環境では緩い設定
if (process.env.NODE_ENV === 'development') {
    app.use(helmet({
        contentSecurityPolicy: false  // 開発時はCSPを無効
    }));
} else {
    // 本番環境では厳格な設定
    app.use(helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'"],
                // 厳格な設定
            }
        }
    }));
}

3. CSPレポートの監視

// CSPレポート受信エンドポイント
app.post('/csp-report', express.json(), (req, res) => {
    const report = req.body;

    // ログに記録
    console.warn('CSP Violation:', {
        timestamp: new Date().toISOString(),
        violatedDirective: report['violated-directive'],
        blockedURI: report['blocked-uri'],
        documentURI: report['document-uri'],
        userAgent: req.get('User-Agent')
    });

    // アラート送信(Slack、メールなど)
    sendCSPAlert(report);

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

まとめ

セキュリティヘッダーは、簡単に実装できて効果が高いセキュリティ対策です。

重要なポイント:

  1. Content-Security-Policy:XSS攻撃の最強の防御手段
  2. Strict-Transport-Security:HTTPS接続の強制
  3. X-Frame-Options:クリックジャッキング攻撃の防御
  4. X-Content-Type-Options:MIMEタイプ詐称攻撃の防御
  5. 段階的な実装:Report-Onlyモードから始める
  6. 定期的な監視:CSPレポートの確認とヘッダーテスト

特に重要なのは、CSPの適切な実装です。最初は緩い設定から始めて、徐々に厳格にしていくことで、既存サイトに影響を与えずに導入できます。

セキュリティヘッダーは「設定するだけ」でWebサイトのセキュリティレベルを大幅に向上させることができる、コストパフォーマンスの高い対策です。今すぐ実装して、より安全なWebサイトを構築しましょう!


コメント

タイトルとURLをコピーしました