サーバーサイドCSPポリシー実施によるXSSからの防御
Lukas Schneider
DevOps Engineer · Leapcell

はじめに
絶えず進化するウェブセキュリティの状況において、クロスサイトスクリプティング(XSS)は依然として最も蔓延し、危険な脆弱性の一つです。攻撃者はXSSの欠陥を悪用して、信頼されたウェブサイトに悪意のあるスクリプトを注入し、それを知らず知らずのうちにユーザーに実行させます。これにより、セッションハイジャック、データ窃盗、改ざん、さらにはウェブサイト全体の defacement につながる可能性があります。クライアントサイドのサニタイズと堅牢な入力検証は不可欠ですが、複雑なXSSシナリオを完全に軽減できないことがよくあります。ここで、Content Security Policy(CSP)は、強力な宣言型のセキュリティレイヤーとして登場します。ブラウザにどのリソースをロードすることを許可するかを指示することにより、CSPは、不正なスクリプト実行を積極的にブロックできるようにする、堅牢な防御メカニズムとして機能します。この記事では、バックエンドフレームワークがCSPヘッダーを効果的に設定し、XSS攻撃に対するウェブアプリケーションセキュリティを大幅に強化する方法を探ります。
コアコンセプトの理解
実装に入る前に、CSPの理解に不可欠ないくつかの重要な用語を明確にしましょう。
- クロスサイトスクリプティング(XSS): 他のユーザーが閲覧するウェブページにクライアントサイドスクリプトを注入することを攻撃者に可能にするセキュリティ脆弱性の一種です。
- Content Security Policy(CSP): ウェブアプリケーション開発者が、特定のページに対してユーザーエージェントがロードすることを許可されるリソース(スクリプト、スタイルシート、画像など)を制御できるW3C標準です。これは、XSSを含む特定の種類の攻撃を軽減するのに役立つ追加のセキュリティレイヤーです。
- HTTPヘッダー: HTTPリクエストまたはレスポンスの一部であり、メッセージに関するメタデータを運びます。CSPは通常、
Content-Security-PolicyHTTPレスポンスヘッダーを介して配信されます。 - ディレクティブ: CSP内のルールであり、さまざまな種類のリソースの許可されるソースを指定します。例としては、
script-src(スクリプト用)、style-src(スタイルシート用)、img-src(画像用)、およびdefault-src(明示的にリストされていないリソースタイプのフォールバック)などがあります。 - ソースリスト: 特定のディレクティブの許可されるオリジンまたはキーワードのリストです。例としては、
'self'(現在のオリジン)、'unsafe-inline'(インラインスクリプト/スタイルを許可、一般的に推奨されない)、'unsafe-eval'(eval()および類似の関数を許可、これも一般的に推奨されない)、およびhttps://example.comのような特定のドメインなどがあります。 - Nonce: 「一度だけ使用される数値」です。CSPでは、暗号学的なnonceを使用して、特定のインラインスクリプトまたはスタイルブロックを明示的に許可し、HTTPレスポンスにリンクして、攻撃者が不正なコードを注入するのをより困難にすることができます。
- ハッシュ: nonceに似ていますが、インラインスクリプトまたはスタイルのコンテンツの暗号学的ハッシュに基づいています。これにより、ブラウザはコンテンツの整合性を検証できます。
バックエンドからのCSPヘッダーの実装
CSPの強みは、サーバーサイドでの実施にあります。バックエンドは、関連するすべてのレスポンスでContent-Security-Policy HTTPヘッダーを生成して送信する責任があります。これにより、ブラウザがHTMLまたはスクリプトコンテンツを処理する前にポリシーが適用されます。
一般的なバックエンドフレームワークを使用した例で説明しましょう。
Python (Flask)
Flaskでは、レスポンスヘッダーを直接設定できます。一般的なアプローチは、デコレータまたはミドルウェアを使用することです。
from flask import Flask, make_response, render_template app = Flask(__name__) @app.route('/') def index(): response = make_response(render_template('index.html')) # 基本的なCSP例:同じオリジンからのスクリプトとスタイルのロードのみを許可し、 # nonceで明示的に許可されない限りインラインスクリプト/スタイルを防ぐ。 # 注意:'unsafe-inline'は開発中に一時的に使用されることが多いですが、削除すべきです。 nonce = generate_nonce() # 実際のアプリケーションでは、これは暗号学的に強力であるべきです csp_policy = ( f"default-src 'self';" f"script-src 'self' 'nonce-{nonce}';" f"style-src 'self' 'nonce-{nonce}';" f"img-src 'self' data:;" # 自己とデータURIからの画像許可 "font-src 'self';" "connect-src 'self';" "frame-ancestors 'none';" # あなたのサイトのフレーミングを防ぐ "form-action 'self';" # フォームの送信先を制限する ) response.headers['Content-Security-Policy'] = csp_policy return response @app.route('/login') def login(): response = make_response(render_template('login.html')) # ページごとに異なるCSPが必要な場合があります nonce = generate_nonce() csp_policy = ( f"default-src 'self';" f"script-src 'self' 'nonce-{nonce}' https://cdnjs.cloudflare.com;" f"style-src 'self' 'nonce-{nonce}';" f"img-src 'self';" "report-uri /csp-report-endpoint;" # 違反報告の例 ) response.headers['Content-Security-Policy'] = csp_policy return response def generate_nonce(): import os import base64 return base64.b64encode(os.urandom(16)).decode('utf-8') if __name__ == '__main__': app.run(debug=True)
そして index.html(またはnonceを使用する他のテンプレート)で:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CSP Protected Page</title> <style nonce="{{ nonce }}"> body { font-family: sans-serif; } </style> </head> <body> <h1>Welcome!</h1> <script nonce="{{ nonce }}"> // このスクリプトは正しいnonceを持っているため実行されます console.log("Inline script executed securely with nonce."); </script> <script src="/static/app.js"></script> <!-- 'self' によって許可 --> <script> // このインラインスクリプトは、'unsafe-inline' または一致するnonceがない場合ブロックされます console.log("This untrusted inline script should be blocked by CSP."); </script> </body> </html>
Node.js (Express)
Expressアプリケーションは、すべてまたは特定のルートに対してCSPヘッダーを設定するためにミドルウェアを使用できます。helmetパッケージは、CSPを含むセキュリティヘッダーに強く推奨されます。
const express = require('express'); const helmet = require('helmet'); const app = express(); const port = 3000; // 暗号学的に強力なnonceを生成する関数 function generateNonce() { return require('crypto').randomBytes(16).toString('base64'); } app.use((req, res, next) => { // 各リクエストに対して新しいnonceを生成します res.locals.nonce = generateNonce(); next(); }); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], // nonceを動的に使用 styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], imgSrc: ["'self'", "data:"], fontSrc: ["'self'"], connectSrc: ["'self'"], frameAncestors: ["'none'"], formAction: ["'self'"], objectSrc: ["'none'"], // Flashのようなプラグインを禁止 // reportUri: "/csp-report-endpoint", // オプション:違反報告用 }, }, })); app.get('/', (req, res) => { // nonceをテンプレートに渡してHTMLテンプレートをレンダリングします res.send(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CSP Protected Node.js Page</title> <style nonce="${res.locals.nonce}"> body { background-color: #f0f0f0; } </style> </head> <body> <h1>Hello from Node.js with CSP!</h1> <script nonce="${res.locals.nonce}"> console.log("Inline script executed securely with nonce in Node.js."); </script> <script src="/static/app.js"></script> </body> </html> `); }); app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); });
CSP実装の主な考慮事項
- レポートオンリーモードから開始: まず、
Content-Security-Policy-Report-Onlyヘッダーを使用して、「レポートオンリー」モードでCSPをデプロイします。これにより、ブロックせずに違反が指定されたURLに報告され、ポリシーの微調整が可能になります。 - 粒度: ディレクティブには可能な限り具体的に指定します。絶対に必要な場合(そして細心の注意を払って)を除き、
'*'や'unsafe-inline'のような広範なソースは避けてください。 - Nonce vs. Hash: インラインスクリプトおよびスタイルでは、スクリプト/スタイルのコンテンツのわずかな変更で破損しやすいため、ハッシュよりもnonceが一般的に好まれます。ただし、ハッシュはコンテンツの整合性を保証します。
- 外部リソース: CDNやサードパーティウィジェットを使用する場合は、それらのドメインが対応するディレクティブに明示的にリストされていることを確認してください。
default-src: このディレクティブはフォールバックとして機能します。リソースタイプが別のディレクティブ(例:script-src)で明示的にカバーされていない場合、default-srcのルールが適用されます。- ユーザー生成コンテンツ: アプリケーションがユーザー生成コンテンツを処理する場合、CSPはさらに重要になります。より厳格なポリシーまたは特定のサニタイズルーチンが必要になる場合があります。
- 反復的な改善: CSPの実装は、しばしば反復的なプロセスです。厳格なポリシーから開始し、レポートを監視し、必要に応じてのみディレクティブを緩和します。
結論
バックエンドからContent Security Policyを実装することは、XSS攻撃から防御するための基本的かつ非常に効果的な戦略です。許可されるコンテンツソースを正確に指示し、動的にnonceを生成することにより、バックエンドフレームワークはアプリケーションに堅牢な最前線の防御を構築する能力を与えます。CSPは万能薬ではありませんが、他のセキュリティベストプラクティスと組み合わせると、攻撃対象領域が大幅に減少し、悪意のあるスクリプトの注入に対してウェブアプリケーションを強化し、より安全なユーザーエクスペリエンスを提供します。最終的に、適切に構成されたCSPは、信頼されたコードのみがユーザーのブラウザ内で実行されることを保証する強力なゲートキーパーとして機能します。

