【実務・中級編】Content-Security-Policy (CSP) の厳格なディレクティブ設計とnonceの活用 – アプリケーションセキュリティ & 安全な開発防御ガイド

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の部分に、サーバーサイドで生成したユニークなランダム文字列を埋め込む。そして、実行したいインラインスクリプトを持つ

ブラウザは、script-srcディレクティブで指定されたnonce値と、スクリプトタグに付与されたnonce値が一致した場合のみ、そのスクリプトの実行を許可する。

実践!nonceを使ったセキュアな実装サンプルコード

ここからは、具体的な実装例を見ていこう。今回は、Webアプリケーションでよく使われるPHP、Python(Flask/Django)、そしてJavaScriptでの例を示す。

1. PHPでの実装例

PHPでは、openssl_random_pseudo_bytes関数などを使って安全なランダム文字列を生成し、それをセッションに保存するか、直接HTMLに埋め込む。





CSP Nonce Example (PHP)




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内の

    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:




    CSP Nonce Example (Node.js/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')で新しい
securityintronationalをフォローする

コメント

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