Content Security Policyの基本と実践 第1回 CSPの基本とJavaScriptへの適用

Content Security Policy(CSP)の基本的な仕組みから、JavaScriptを安全に制御するためのnonce/hash/strict-dynamicの使い方まで解説します。

発行

著者 渡辺 由 フロントエンド・エンジニア
Content Security Policyの基本と実践 シリーズの記事一覧

はじめに

Webアプリケーションの制作・運用にあたって、セキュリティの担保のためにContent-Security-Policy(CSP)について調べたり、導入したりといった経験のある方は多いのではないでしょうか。

CSPは、Webページが読み込むリソースを制限することで、クロスサイトスクリプティング(XSS)などの攻撃からユーザーを守るためのセキュリティ機能です。フロントエンドに発生しうる脆弱性として、XSSはもっとも代表的なものの1つですので、フォームから入力されたデータや、Cookieの取り扱いなどに注意して実装する必要はあります。とはいえ、どんなに注意していても、絶対に大丈夫、という保証はありませんし、利用しているサードパーティー製のライブラリに脆弱性がある可能性もあります。CSPを設定しておけば、万が一コードに脆弱性があったとしても、攻撃を防げる可能性が相当に高まります。

XSSについて

XSSについては、次のシリーズで詳しく解説しています。

CodeGridでも2016年にフロントエンド開発のためのセキュリティというシリーズでCSPを紹介しています。CSPの基本的な仕様は今も変わっていませんが、10年のあいだにはCSP Level 2、Level 3とアップデートを重ねており、より柔軟で効果的な制御が可能になっています。以前は、安全のためにCSPで許可する範囲を絞りすぎると、必要なスクリプトまで動かなくなる、といった経験が筆者にもありました。アップデートに伴い、「危険を抑えつつ、必要なスクリプトは動かす」という理想的なポリシー設定もしやすくなってきました。また、最近ではAstroやSvelteKitといったフレームワークもCSPをサポートしており、導入も容易になっています。

この記事ではCSPについて改めて整理していきます。CSPの全体像やJavaScriptに関する設定例に始まり、SvelteKit、Astro、Next.jsといったWebフレームワークでの対応についても取り上げます。1回目となる今回は、CSPの基本とJavaScriptへの適用について紹介します。2回目では、スクリプト以外のリソースへのCSP適用を、3回目ではWebフレームワークのCSP対応を取り上げます。

CSPの仕組み

まず、CSPがどのようにブラウザへ伝わり、どう機能するかを確認しておきましょう。CSPは、HTTPレスポンスヘッダーContent-Security-Policyで指定します。たとえば次のような指定です。

CSPヘッダーの例

Content-Security-Policy: default-src 'self'; img-src 'self' https://cdn.example.com

ポリシーの記述方法については後述しますが、このようなヘッダーを受け取ったブラウザは、ページ内のリソースがポリシーに反していないかをチェックし、違反する読み込みや実行を拒否します。その結果、Webページの配信元が意図していない外部リソースは実行されないので、仮に悪意あるスクリプトが注入されても実行されることはありません。

CSPの対象となる「C」つまりコンテンツは、Webページで読み込まれるあらゆるリソースで、スクリプトやスタイルシート、画像、iframe、フォントなど、そのページが読み込む外部リソースが丸ごと対象になります。

ポリシーの定義

ポリシーを定義するには、セミコロン区切りで複数のディレクティブを並べて記述します。各ディレクティブには、directive-name source1 source2 ...の形で、名前のあとに半角スペース区切りで許可するソースを列挙します。いわゆるホワイトリスト方式です。

代表的なソース値には次のようなものがあります。

  • 'self':同一オリジンのリソースのみを許可
  • 'none':何も許可しない
  • https://cdn.example.com:特定のホストを許可
  • https::特定のスキームを許可

'self'のようなキーワードはシングルクオートで囲む必要があります。極端な例ですが、script-src'none'を指定すると、JavaScriptの読み込みは一切禁止、という挙動になります。

default-srcとフォールバック

script-srcstyle-srcなど、ディレクティブは多数ありますが、そのすべてを個別に指定する必要はありません。default-srcを指定しておくと、個別に指定のないディレクティブはこの値にフォールバックします。

default-srcで全体を絞り込む

Content-Security-Policy: default-src 'self'

このように書いておけば、script-srcstyle-srcimg-srcも、すべてに'self'が適用されます。その上で、画像やWebフォントの配信元だけは外部CDNも許可したい、という場合は、次のように個別に上書きすればよいです。

default-srcをベースに、img-srcだけ上書き

Content-Security-Policy: default-src 'self'; img-src 'self' https://cdn.example.com

metaタグでの指定

CSPは<meta http-equiv="content-security-policy">のようにHTML文書のヘッダーにも指定できます。ただし、記述できるディレクティブはHTTPヘッダーのほうが多く、iframeについての指定であるframe-ancestorsなどはmetaタグでは指定できません。また、HTTPヘッダーとmetaタグの両方にCSPを指定した場合、同じリソースに関するポリシーが複数あるとより厳しいほうが適用されます。どちらも有効ではありますが、混乱を避けるためにどちらか一方に定義するのがよいでしょう。

metaタグ指定で大丈夫?

「metaタグ指定のほうが古い仕様」、「HTTPヘッダーのほうが優先される」といった情報を見かけたことがある方もいるかもしれませんが、そんなことはありません。技術上の制約で指定できないディレクティブもありますが、metaタグでの指定自体が非推奨になっているわけではありません。

3回目で取り上げるフレームワークでの対応状況を見ても、たとえばAstroはmetaタグでの指定をサポートしています。ですので、HTTPヘッダーの設定が難しい環境であれば、metaタグでの指定も現実的な選択肢です。

JavaScriptでの指定例と問題点

CSPがさまざまな外部リソースを制限できることをお話ししましたが、ここからはまずセキュリティ面でもっとも重要なJavaScriptを取り上げます。CSPのアップデートに伴い、より柔軟な指定が可能になり、使いやすくなっています。具体的に見ていきましょう。

前述のとおり、JavaScriptを許可するためのディレクティブはscript-srcです。JavaScriptがまったく使われていないサイト、というのもあまりないと思いますので、次のような指定が一般的で、安全性も高いです。

selfだけを許可するCSP

Content-Security-Policy: script-src 'self'

'self'、つまりは同一オリジンから配信されるJavaScriptだけを許可する、という意味になります。これであれば、攻撃者が外部ドメインにある悪意あるスクリプトを読み込ませることはできませんし、HTMLにインラインでスクリプトを埋め込んでも動きません。

実際には、'self'では制限が厳しすぎることもあるでしょう。スクリプトがサブドメインでホストされていたり、外部のCDNから読み込んでいたりする場合は、次のようにホストを指定して許可することもできます。

特定の外部ドメインも許可するCSP

Content-Security-Policy: script-src 'self' https://cdn.example.com

このような指定になることもまた、よくあるでしょう。自身が管理するサーバーや、信頼できるCDNサービスであれば問題ありませんが、サードパーティー製の不透明な配信元を許可するのは、リスクがありますので注意が必要です。

制限を緩める指定はほかにもあり、たとえば'unsafe-inline'はインラインスクリプトを許可する指定、'unsafe-eval'eval()new Function()の使用を許可する指定です。unsafe-という接頭辞のとおり、これらはセキュリティ上のリスクが非常に高い指定です。許可してしまうと、いわゆるXSS攻撃を防げなくなるためです。

とはいえ、実際はインラインスクリプトを使いたい場面もありますし、依存するJavaScriptライブラリがeval()を使用している場合もあるでしょう。筆者が困ったことがあるのはGoogle Analyticsにデータを送信する場合です。サービスのダッシュボードでは、「以下のコードをサイトに貼り付けてください」といった形でJavaScriptコードが提供されるのですが、これはunsafe-inlineを許可しないと動きません。さらには、このコードは外部にあるJavaScriptを読み込む形になっているため、script-srcは次のような指定になってしまいます。

Google Analyticsのコードを動かすためのCSP設定例

script-src 'unsafe-inline' https://www.googletagmanager.com;

より安全なスクリプト指定の方法

そこで有効なのが、CSP Level 2以降で追加されたnonce/hash/strict-dynamicです。それぞれの仕組みを見ていきましょう。

nonce方式

nonceは「number used once」の略で、サーバーがリクエストごとにランダムな文字列を生成し、その文字列を知っているスクリプトだけを許可する方式です。

読みは「ナンス」または「ノンス」です。日本語の記事ではどちらも見かけますが、筆者はなんとなく「ナンス」のほうが英語の読みに近い気がしていて、こちらで喋ることが多いです。

次のように使います。

nonceを使ったCSP

Content-Security-Policy: script-src 'nonce-r4nd0mstring'

r4nd0mstringがサーバーで生成されたランダム文字列です。これをインラインスクリプトにも付与します。

nonceを付与したscriptタグ

<script nonce="r4nd0mstring">
  // ... 
</script>

nonceがリクエストごとに変わっていれば、攻撃者がこの値を予測することは非常に難しいため、インラインスクリプトを安全に許可できます。また、外部スクリプトを読み込む場合でも、<script src="..." nonce="r4nd0mstring">のようにnonceを付与して許可できます。サードパーティー製のスクリプトを読み込む際には、先ほどの例のように外部サービスやCDNのホスト名を丸ごと許可してしまうよりも、nonceを付与して特定のスクリプトだけを許可するほうが安全です。

hash方式

hash方式は、スクリプトの内容そのもののハッシュ値を計算し、その値と一致するスクリプトだけを許可する方式です。CSP以外でも、ファイルの完全性を検証するために使われるやり方ですね。今度はヘッダーへの指定のみで許可できます。

hashを使ったCSP

Content-Security-Policy: script-src 'sha256-xxxxxxxxxx...'

nonceと異なり、リクエストごとに値を生成する必要がありません。代わりに、スクリプトの内容が変わるたびにハッシュ値の再計算が必要ですが、フレームワークによっては、ビルド時に自動でハッシュを計算してくれるものもあります。静的サイトなど、スクリプトの内容が頻繁に変わらないケースであれば非常に便利な方式です。

strict-dynamic

さらにCSP Level 3では'strict-dynamic'というキーワードが追加されました。これは、noncehashで信頼されたスクリプトが動的に読み込む別のスクリプトも自動的に信頼する、という仕組みです。先ほどのGoogle Analyticsの例にも適用できます。

strict-dynamicを含むCSP

Content-Security-Policy: script-src 'nonce-r4nd0mstring' 'strict-dynamic'

これならunsafe-inlineもいりませんし、外部のホスト名を指定する必要もなくなりました。もしも、呼び出す外部スクリプトのURLが変わったり、増えたりしても変更は不要です。シンプルかつ安全に、サイトに必要なスクリプトだけを許可できるようになりました。

script-src以外の許可も必要

実際にGoogle Analyticsのコードを動作させるには、使用する機能にもよりますが、img-srcconnect-srcの許可も必要です。これらのディレクティブについては2回目で取り上げます。

'strict-dynamic'は、スクリプトを分割して動的に読み込んでいるような場合にも便利な指定です。フレームワークやビルドツールによっては、1つのJavaScriptファイルが大きくならないよう、自動的に分割する設定がありますが、そのような場合に'strict-dynamic'を指定しておけば、すべてのファイルにnoncehashを付与する手間はなくなります。なお、'strict-dynamic'を指定すると、'self'やホスト指定は無視されるので、注意が必要です。nonceまたはhashで信頼されたスクリプトだけが、別のスクリプトを読み込むことができるようになります。

導入のテストとレポート機能

さて、CSPは強力な仕組みですが、実際にさまざまな指定を試していると、必要なスクリプトまでブロックしてしまってサイトの機能が動かない……といった問題が発生することがあります。そこで便利なのが、CSPのレポート機能です。ポリシー違反があってもブロックはせず、報告だけを送信する機能で、まずはこれを使ってテストしてみるのがよいでしょう。

指定方法はシンプルで、HTTPヘッダーの名前がContent-Security-Policy-Report-Onlyに変わるだけです。

Report-Onlyの指定例

Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint;

1行目のReporting-Endpointsヘッダーは、Reporting APIを使ってレポートの送信先を指定するためのヘッダーです。2行目以降がCSPのポリシーとレポートの指定です。ポリシーに違反するリソースがあった場合、ブラウザはリクエストをブロックせず、application/reports+jsonというMIMEタイプのドキュメントをhttps://example.com/csp-reportsに送信します。

以前は、report-uriというディレクティブに直接URLを記述していましたが、Reporting-Endpointsヘッダーとreport-toを併用するのが新しい方法で、2026年3月にはすべてのブラウザが対応しています。古いブラウザのために、report-uriを併記することもできます。

ここまでのまとめ

この記事では、現在利用可能なCSPの機能と、JavaScriptを安全に扱うためのnoncehashstrict-dynamic、そしてレポーティングについてお話ししました。

いまJavaScriptに関するCSPを設定するなら、可能であればnoncehashを使いたいところです。ビルド・ホスティング環境やフレームワークが対応していれば、それほど手間をかけずにセキュリティ上の安心が得られるので、メリットが大きいと考えます。難しい場合は、'self'に加え、必要最低限の外部ホストを許可するのがよいでしょう。unsafe-がつく指定は、できるだけ避けるべきです。

2回目では、スクリプト以外のリソースへのCSP適用について、3回目ではSvelteKit、Astro、Next.jsでのCSP対応について整理していきます。