XSSの最後の砦!CSPの”nonce”でインラインスクリプトを安全に解放する
おい、諸君。今日もコードと格闘してるか?サイバー攻撃の最前線で、日々繰り広げられる巧妙な手口に頭を悩ませていることだろう。特に、Webアプリケーションにおけるクロスサイトスクリプティング(XSS)は、古くて新しい、そして根深い脅威だ。ユーザーのブラウザ上で悪意のあるスクリプトが実行されるとなれば、セッションハイジャック、情報漏洩、さらにはサイトの改ざんまで、被害は計り知れない。
我々が日々取り組んでいるのは、単なる「バグ修正」じゃない。これは、サイバー空間における「防御」なんだ。そして、その防御の要となる技術の一つが、Content Security Policy (CSP) だ。
CSPの基本:なぜunsafe-inlineは諸悪の根源なのか
CSPは、ブラウザに対して、どのオリジンからのコンテンツ(スクリプト、スタイルシート、画像など)を読み込んで実行して良いかを指示する、強力なセキュリティメカニズムだ。HTTPヘッダー、あるいはHTMLのタグで設定できる。
多くの開発者が、XSS対策としてまず思いつくのがCSPの導入だろう。しかし、ここで多くの人が陥る落とし穴がある。それは、unsafe-inline の使用だ。
例えば、こんな風に設定しているケースを見かける。
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘unsafe-inline’; style-src ‘self’ ‘unsafe-inline’;
script-src 'self' 'unsafe-inline' は、「同じオリジン('self')からのスクリプトと、HTML内に直接記述されたインラインスクリプト('unsafe-inline')を許可する」という意味になる。
問題は、この'unsafe-inline'だ。 これを許可してしまうと、たとえscript-src 'self'で外部スクリプトを制限したとしても、攻撃者は巧妙にHTML内に悪意のあるインラインスクリプトを挿入できてしまう。
具体的な攻撃シナリオを想像してみよう。
あるWebアプリケーションで、ユーザーが入力したテキストをそのまま画面に表示する機能があったとする。ここで、攻撃者は以下のようなJavaScriptコードを投稿する。
もし、このサイトのCSPがscript-src 'self' 'unsafe-inline'だとすると、ブラウザはこのインラインスクリプトを正当なものとして実行してしまう。結果、ユーザーのブラウザには「XSSed!」というアラートが表示される。これはまだ可愛い例だが、実際には、
fetch()APIを使って、ユーザーのセッショントークンを攻撃者のサーバーに送信する。document.cookieを盗み出し、ログイン情報を窃取する。タグを動的に生成し、ユーザーを偽のログインページにリダイレクトさせる。
といった、より悪質な攻撃が可能になる。
unsafe-inlineは、XSS防御の「 pintu bocor(漏れた扉)」だ。 ここを塞がなければ、CSPを導入した意味が半減どころか、むしろ「対策したつもり」という油断を生んでしまう。
nonce(ナンス)でインラインスクリプトを「安全に」解放する
では、どうすればインラインスクリプトの利便性を維持しつつ、XSSの脅威から逃れられるのか?そこで登場するのが、nonce(ナンス) だ。
Nonceとは「Number used once」の略で、一度しか使われないランダムな値のことだ。CSPでは、このnonceをスクリプトタグに付与することで、そのスクリプトの実行を許可する。
攻撃者は、このランダムなnonce値を事前に知ることはできない。 したがって、たとえインラインスクリプトを挿入できたとしても、正しいnonce値がなければ実行されることはない。
CSP設定の例:
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-RANDOM_NONCE_VALUE’; style-src ‘self’;
ここで、RANDOM_NONCE_VALUEの部分に、サーバーサイドで生成したユニークなランダム文字列を埋め込む。そして、実行したいインラインスクリプトを持つタグにも、同じnonce属性を付与する。
ブラウザは、script-srcディレクティブで指定されたnonce値と、スクリプトタグに付与されたnonce値が一致した場合のみ、そのスクリプトの実行を許可する。
実践!nonceを使ったセキュアな実装サンプルコード
ここからは、具体的な実装例を見ていこう。今回は、Webアプリケーションでよく使われるPHP、Python(Flask/Django)、そしてJavaScriptでの例を示す。
1. PHPでの実装例
PHPでは、openssl_random_pseudo_bytes関数などを使って安全なランダム文字列を生成し、それをセッションに保存するか、直接HTMLに埋め込む。
Content Security Policy with Nonce
Check the browser's developer console for output.
解説:
bin2hex(openssl_random_pseudo_bytes(32))で、強力なランダムな16進数文字列を生成しています。これは、攻撃者が推測するのが非常に困難です。header()関数でCSPヘッダーを設定し、script-srcディレクティブに'nonce-と生成した$nonce値を結合して指定しています。- HTML内の
タグに、PHPで生成した$nonce値をnonce属性として埋め込んでいます。これにより、ブラウザはこのスクリプトの実行を許可します。 - コメントアウトしているように、JavaScript側で
document.currentScript.getAttribute('nonce')を使えば、実行中のスクリプトのnonce値を取得することも可能です。
2. Python (Flask) での実装例
Flaskでは、Jinja2テンプレートエンジンが利用できるため、nonceの生成と埋め込みが比較的容易です。
from flask import Flask, render_template, make_response import os import secrets # Python 3.6+ 推奨
app = Flask(__name__)
@app.route('/') def index(): # セキュアなランダム文字列を生成 (32バイト) nonce = secrets.token_hex(32)
# CSPヘッダーを生成 # script-src ディレクティブに 'nonce-' と生成したnonce値を指定 csp_policy = f"default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self';"
# レスポンスを作成し、CSPヘッダーを設定 response = make_response(render_template('index.html', csp_nonce=nonce)) response.headers['Content-Security-Policy'] = csp_policy return response
if __name__ == '__main__': # 本番環境ではdebug=Falseにし、適切なホストとポートを指定してください app.run(debug=True)
templates/index.html:
Content Security Policy with Nonce
Check the browser's developer console for output.
解説:
secrets.token_hex(32)で、安全なランダムな16進数文字列を生成します。os.urandom(32)なども利用可能です。- 生成したnonce値を
csp_policy文字列に埋め込み、response.headers['Content-Security-Policy']でHTTPヘッダーに設定しています。 - Jinja2テンプレートでは、
{{ csp_nonce }}という記法で、Python側で渡されたnonce値をHTMLのnonce属性に埋め込んでいます。
3. JavaScriptでの実装例 (クライアントサイドでの動的なnonce生成・適用)
サーバーサイドでCSPヘッダーを設定するのが基本ですが、場合によってはクライアントサイドでJavaScriptを使って動的にスクリプトを生成し、nonceを適用する必要があるかもしれません。ただし、この方法は、「最初のCSPヘッダー設定」 が前提となります。
まず、サーバーサイドで以下のようなCSPヘッダーを返していると仮定します。
サーバーサイド (例: Node.js/Express):
const express = require('express');
const crypto = require('crypto'); // Node.jsのcryptoモジュール
const app = express();
const port = 3000;
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('hex'); // 16バイト = 32文字の16進数
res.setHeader('Content-Security-Policy', default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self';);
// nonce値をリクエストオブジェクトに付加しておくと、後で使いやすい
req.csp_nonce = nonce;
next();
});
app.get('/', (req, res) => {
// nonce値をテンプレートエンジンに渡す (例: EJS)
res.render('index', { csp_nonce: req.csp_nonce });
});
// 静的ファイルの設定 (例: /static/js/main.js)
app.use('/static', express.static('public'));
// テンプレートエンジンの設定 (例: EJS)
app.set('view engine', 'ejs');
app.set('views', './views'); // viewsディレクトリにindex.ejsを配置
app.listen(port, () => {
console.log(Server running at http://localhost:${port});
});
views/index.ejs:
Content Security Policy with Nonce
Check the browser's developer console for output.
解説:
- サーバーサイドで生成したnonceを、テンプレートエンジン(この例ではEJS)を通じてHTMLに埋め込みます。
- JavaScript側では、
document.querySelector('script[nonce]')などで既存のスクリプトタグからnonce値を取得するか、あるいはサーバーからhidden inputなどで明示的にJavaScript変数として渡す方法があります。 document.createElement('script')で新しい要素を作成し、取得したnonce値をscriptElement.nonceで設定します。scriptElement.textContentに実行したいJavaScriptコードを記述し、document.body.appendChild(scriptElement)でDOMに追加することで、スクリプトが実行されます。
注意点: クライアントサイドでの動的なスクリプト生成は、nonceを正しく管理しないと、かえって脆弱性を生む可能性があります。サーバーサイドでCSPヘッダーとしてnonceを設定し、それをクライアントサイドで参照する、という流れを徹底することが重要です。
4. WAF/Nginx/クラウドIAMでの設定例
CSPは、WebサーバーやWAF、クラウドのIAM(Identity and Access Management)サービスでも設定可能です。
Nginxの設定例:
nginx.conf またはサイト設定ファイル内のhttp, server, locationブロックに以下を追加します。
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-RANDOM_NONCE_VALUE'; style-src 'self';" always;
注意: Nginxで動的にnonceを生成するには、Luaスクリプトなどを組み込む必要があり、設定はやや複雑になります。多くの場合、アプリケーションサーバー側でnonceを生成し、Nginxはそれをそのままヘッダーに付加する(proxy_passなどを使用している場合)か、静的なadd_headerディレクティブで設定します。
AWS WAFの設定例 (例: Managed Ruleset + Custom Rule):
AWS WAFでは、CSPヘッダーを検査したり、特定のCSPディレクティブを強制したりするルールを設定できます。
- AWS WAF Managed Ruleset: 「OWASP Top 10」などのマネージドルールセットには、XSS対策に関連するルールが含まれています。
- Custom Rule:
- Rule builder:
Allowアクションで、Custom responseを設定し、CSPヘッダーを追加するルールを作成します。 - Nonceの管理: nonce自体はAWS WAFで直接生成・管理するのではなく、アプリケーション側で生成し、WAFはそのnonceを含むリクエストのみを許可するように設定するか、あるいはWAFでCSPヘッダーを付与するような設定を行います。
クラウドIAM (例: Google Cloud IAM / Azure AD):
IAMサービス自体が直接CSPヘッダーを管理するわけではありませんが、これらのサービスと連携するWebアプリケーションやAPIゲートウェイでCSPを設定する際に、IAMの権限管理と組み合わせて、CSP設定が変更されないように保護する、といった間接的な連携は考えられます。
重要なのは、CSPの設定場所に関わらず、「nonceを安全に生成し、リクエストごとにユニークな値を使い、それをスクリプトタグに正しく適用する」という原則を守ることです。
さらなる厳格化:unsafe-evalの排除とstrict-dynamic
nonceの導入は、unsafe-inlineを排除するための強力な一歩です。しかし、現代のWebアプリケーションでは、eval()関数やsetTimeout(), setInterval()の文字列引数、あるいはnew Function()のような、実行時にコードを生成・評価する機能(unsafe-eval) が使われることもあります。
これらもまた、XSSの温床となりうるため、CSPではunsafe-evalをディレクティブで明示的に禁止することが強く推奨されます。
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM_NONCE_VALUE'; style-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';
さらに、最近のブラウザでは strict-dynamic というディレクティブも登場しています。これは、信頼されたスクリプト(nonce付きなど)が動的にロードしたスクリプトは、たとえそれがインラインスクリプトであっても(nonceなしでも)許可するというものです。
Content-Security-Policy: script-src 'self' 'nonce-RANDOM_NONCE_VALUE' 'strict-dynamic';
strict-dynamicは、SPA(Single Page Application)フレームワークなどで動的にスクリプトがロードされる場合に有効ですが、その挙動を完全に理解し、意図しないスクリプトの実行を招かないように注意深く導入する必要があります。まずはunsafe-inlineとunsafe-evalを排除し、nonceを徹底することが最優先です。
まとめ:日々の運用で意識すべきこと
unsafe-inlineとunsafe-evalは悪魔の囁きだ。 XSS対策の基本は、これらをCSPディレクティブで明確に禁止することから始まる。- nonceはXSS防御の「秘密兵器」。 リクエストごとにユニークで予測不可能なnonceを生成し、それをCSPヘッダーとスクリプトタグに適用することで、インラインスクリプトを安全に実行できる。
- コード例を参考に、あなたの開発言語・フレームワークに合わせた実装を。 PHP、Python、Node.jsなど、各言語でのnonce生成とCSPヘッダー設定の方法を理解しよう。
- WebサーバーやWAFの設定も活用する。 NginxやAWS WAFなどの設定でCSPを適用することで、アプリケーションコードの変更なしにセキュリティを強化できる場合がある。
- 常に最新のOWASP Top 10をチェックする。 サイバー攻撃の手法は日々進化している。我々もまた、常に学び続け、防御策をアップデートしていく必要がある。
CSPとnonceの導入は、確かに初期実装の手間はかかるかもしれない。しかし、一度堅牢な仕組みを構築してしまえば、XSS攻撃に対する防御力が格段に向上する。これは、ユーザーを守り、サービスを守り、そして何より、我々自身が安心して開発に集中できる環境を作るための、投資なんだ。
さあ、諸君。今日のコードに、この「nonce」という名の堅牢な壁を築き上げようじゃないか。何か不明な点があれば、いつでも声をかけてくれ。
コメント