Webセキュリティ

Content-Security-Policy(CSP)完全ガイド — XSS攻撃を防ぐHTTPヘッダー

セキュリティ CSP Web

Content-Security-Policy(CSP)完全ガイド

CSPはXSS攻撃を防ぐHTTPレスポンスヘッダー。どのリソースの読み込みを許可するかをブラウザに指示する。

この記事の対象読者

  • XSS対策を強化したいWebエンジニア
  • Nginx / Django でCSPを導入したい方
  • CSPの違反レポートを活用してセキュリティ運用を行いたい方
  • 既存サイトに段階的にCSPを導入したい方

Step 1CSPとは

Content-Security-Policy(CSP)は、Webページがどのリソースを読み込んでよいかをブラウザに指示するHTTPレスポンスヘッダーです。サーバーが「このページでは、自分自身のドメインのスクリプトだけ実行してよい」と宣言することで、攻撃者が注入した不正なスクリプトの実行をブラウザレベルでブロックできます。

XSS(クロスサイトスクリプティング)攻撃は、Webアプリケーションの脆弱性を突いて、ユーザーのブラウザ上で悪意のあるスクリプトを実行する攻撃です。入力値のエスケープやサニタイズが第一の防御策ですが、CSPはその「二重防御」として機能します。

CSPの本質: CSPはアプリケーションの脆弱性を修正するものではありません。脆弱性が悪用された場合の被害を軽減する「緩和策」です。入力値のバリデーションやエスケープ処理と組み合わせて使いましょう。

Step 2主要ディレクティブ一覧

CSPはディレクティブと呼ばれる命令の集合で構成されます。各ディレクティブが特定の種類のリソースに対するポリシーを定義します。

ディレクティブ対象リソース設定例
default-srcフォールバック'self'
script-srcJavaScript'self' https://cdn.example.com
style-srcCSS'self' 'unsafe-inline'
img-src画像'self' data: https:
font-srcフォント'self'
connect-srcXHR, Fetch, WebSocket'self' https://api.example.com
frame-ancestorsiframe埋め込み制御'none'
base-uribase タグ'self'
form-actionフォーム送信先'self'
object-srcobject, embed'none'

注意: default-src はフォールバックであり、個別のディレクティブが指定されている場合はそちらが優先されます。default-src 'none' として必要なものだけ個別に許可するのが最も安全です。

Step 3設定方法

Nginx での設定

nginx.conf
server {
    add_header Content-Security-Policy
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
        always;
}

Django ミドルウェアで設定

middleware.py
class CSPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        response["Content-Security-Policy"] = (
            "default-src 'self'; "
            "script-src 'self'; "
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data:; "
            "font-src 'self'; "
            "connect-src 'self'; "
            "frame-ancestors 'none'"
        )
        return response

HTML メタタグでの設定

index.html
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'">

メタタグの制限: メタタグでは frame-ancestorsreport-uri を使用できません。本番ではHTTPヘッダーを推奨。

Step 4よく使う設定パターン

静的サイト(最も厳格)

CSPヘッダー
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

SPA(React / Vue)

CSPヘッダー
default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https://api.example.com wss://ws.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

Step 5unsafe-inline と unsafe-eval

'unsafe-inline'script-src に指定すると、ページ内のすべてのインラインスクリプトが実行可能になり、CSPの保護効果が大幅に低下します。

重要: script-src'unsafe-inline' を指定すると、CSPによるXSS防御はほぼ無効化されます。可能な限りnonce または hash ベースに切り替えましょう。

代替手段: nonce(推奨)

Django ミドルウェア(nonce生成)
import secrets

class CSPNonceMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        nonce = secrets.token_urlsafe(32)
        request.csp_nonce = nonce
        response = self.get_response(request)
        response["Content-Security-Policy"] = (
            f"default-src 'self'; "
            f"script-src 'self' 'nonce-{nonce}' 'strict-dynamic'; "
            f"style-src 'self' 'nonce-{nonce}'; "
            f"img-src 'self' data:; font-src 'self'; "
            f"connect-src 'self'; frame-ancestors 'none'"
        )
        return response
テンプレート
<script nonce="{{ request.csp_nonce }}">
    console.log("nonce付きなので実行される");
</script>

<script>
    alert("XSS"); // nonceがないのでブロックされる
</script>

Step 6違反レポート(report-uri / report-to)

CSPにはポリシー違反時にレポートを送信する機能があります。

CSPヘッダー
Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint
Django: レポート受信エンドポイント
import json, logging
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

logger = logging.getLogger("csp")

@csrf_exempt
def csp_report(request):
    try:
        report = json.loads(request.body)
        logger.warning("CSP違反: %s", report.get("csp-report", {}).get("blocked-uri"))
    except json.JSONDecodeError:
        pass
    return HttpResponse(status=204)

Step 7段階的な導入手順

既存サイトにCSPをいきなり適用するとサイトが壊れる可能性があります。以下の手順で段階的に導入しましょう。

Report-Only モードで現状を把握
違反レポートを分析し、ポリシーを調整
エンフォースモードに切り替え
継続的にレポートを監視・改善
Nginx(Report-Onlyモード)
add_header Content-Security-Policy-Report-Only
    "default-src 'self'; script-src 'self' 'unsafe-inline'; report-uri /csp-report-endpoint"
    always;

最終目標: 理想的なCSPは default-src 'none' をベースに、必要なリソースだけをホワイトリストで許可し、インラインスクリプトはnonceで管理する構成です。段階的に改善していきましょう。