ゼロから学ぶCSP(Content Security Policy)入門

ゼロから学ぶCSP(Content Security Policy)入門

のっぴー

のっぴー

Technology部ののっぴーです。

ブラウザにはセキュリティに関する様々な仕組みがあり、その一つとしてXSSのリスク軽減を目的としたセキュリティレイヤーとしてCSP(Content Security Policy)があります。昨今ではCSPが脆弱性診断の対象となることもあり、適切な設定を求められている事業者もいらっしゃるかと思います。
※XSS:クロスサイトスクリプティングの略語。サイトの脆弱性を利用して悪質なスクリプトを埋め込み、ユーザーの個人情報を抜き取ったり、マルウェア感染を図ったりすること

そこで今回は、CSPの目的や設定方法など、基礎から丁寧に解説していきます。ご参考になれば幸いです。

1分で学ぶCSPの概要

まずは最低限押さえておきたい概要に絞って、CSPの基礎をご紹介します。

CSP(Content Security Policy)とは

ブラウザ上でのコンテンツ読み込みを制限して、XSSやデータインジェクションなどのリスクを軽減する仕組みのことです。CSPと略されることが多いです。

CSP対応として行うこと

HTTPのContent-Security-Policyレスポンスヘッダー(以下CSPヘッダー)を適切に設定することです。

CSPでできること

CSPヘッダーの設定でリソース(画像、動画、iframe等)読み込みやスクリプト実行を制限できます。

CSPヘッダーがある場合とない場合の比較

サンプルとしてWebサイト(ex:https://example.com/index.html)にアクセスした際の、CSPヘッダーの有無によるブラウザ側動作を比較しました。

・CSPヘッダーなしの場合

index.htmlに記載している全ての外部リソース(imgタグやscriptタグ)が読み込まれます。scriptタグについては読み込み後に実行されます。

・CSPヘッダーありの場合

index.htmlに記載されているすべての外部リソースのうち、CSPに違反していないリソースのみ読み込みと実行が行われます。

上の画像の例では、img-src ‘self’と指定されているため、同一オリジンからの画像リソースのみ許可され、https://bbb.com/BBB.jpg の読み込みは拒否されます。

また、script-src nonce-xxxxと指定されているため、nonce値(※nonceの詳細は後述)が一致するccc.jsはスクリプトが実行されますが、ddd.js はnonce値がなくCSP違反となるため実行されません。
※仮にinlineでスクリプトが記載されていても、CSP違反の場合は実行はされません。

CSPの詳しい説明

CSPの目的

冒頭で説明した通り、XSS対策がメインです。

CSPを設定することで信頼するスクリプトのみを実行し、その他のスクリプトの実行を拒否(無視)できます。また、画像など利用できるリソースや利用プロトコルの制限をすることで、サービス提供者の意図しないコンテンツの読み込みを防ぐことが可能です。

CSPは多層防御としての利用を想定されています。そのため、CSPでXSS対策をしている場合でも他の防御手法(ex:入出力時のサニタイズ・バリデーション等)による対応は行うべきです。

CSPの設定概要

2023年12月時点、CSPでは5種類のディレクティブが存在します。一番利用頻度が高く、重要なものはFetchディレクティブです。ここでは簡潔に全体図と一例を記載しますので、さらに詳しく知りたい方はMDNのリファレンスを参照してください。

Fetchディレクティブ

Fetchディレクティブには、フォールバックがあります。

例えばstyle-srcが指定されていない場合、そのフォールバック先であるdefault-srcの値が使用されます。child-srcのみ特殊で、フォールバック元がframe-srcかworker-srcかでフォールバック先が異なります。

ツリー構造風に表すと下記の図のようになり、右のものが優先されます。

script-src以外はホワイトドメインで指定し、読み込み元を制限するのが一般的です。script-srcは strict-dynamic + nonceによる制限がいいとされています(strict-dynamic + nonceの組み合わせはStrict CSPと呼ばれています)。

下記は画像の読み込み元を自身のサイトと「https://*.example.net」のみに制限する例です。

Content-Security-Policy: img-src 'self'; https://*.example.net;

 
脆弱性診断ツールによってはホワイトリストドメインにワイルドカードを使うと脆弱性扱いしてくるものもあります(個人的には厳しすぎると思います)。

Documentディレクティブ

ドキュメントやワーカー環境のプロパティを管理します。2023年12月時点ではbase-uriとsandboxの2つのディレクティブが存在します。

<base>タグに意図しない値を設定されないよう、base-src ‘self’またはbase-src ‘none’を設定することが多いです。

Navigationディレクティブ

<form>や<iframe>などの動作の制限を行えます。
詳細はMDNのナビゲーションディレクティブの項目を参照してください。

Reportingディレクティブ

CSP違反の報告過程を制御します。

主な利用方法としては、Content-Security-Policy-Report-OnlyヘッダーにFetchディレクティブと同時にreport-uri(非推奨)またはreport-toディレクティブを設定することで、CSP違反となった情報をJSON形式で受け取ることができるようになります(CSPレポートはブラウザから送信されます)。

# 例
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://example.com/csp-report;

その他のディレクティブ

スキーマ名がhttpでも、セキュリティで保護された HTTPSを介して提供されるURLで置き換えたかのように処理する指示をおこなうupgrade-insecure-requestsなどがあります。

CSP Levelについて

CSP Level 2

level2では信頼できるドメインをホワイトリストで指定する方法が主流でした。ただし、信頼できるドメインからCSPをバイパスできることが多く、現在ではXSS対策としては不十分とされています。

nanceも利用できましたが、すべてのスクリプト(特に動的生成されたもの)にnonceを付与するのは非常に高コストかつ困難だったという欠点がありました。

CSP Level 3

2023年12月時点では CSP Level 3 のDraftが公開されています。
モダンブラウザでは既に実装済みのものも多いです。

Level3ではstrict-dynamic ディレクティブが追加されました。これはnonceが指定されたスクリプトから動的に生成された子スクリプトの実行を許可するものです。

利用した場合、ドメインのホワイトリストは無視されます(対応していないブラウザでは逆にstrict-dynamic が無視されます)。基本的には、nonce + strict-dynamicの組み合わせが推奨されており、GoogleではStrict CSPとして説明されています。

サンプルWEBアプリケーションを使ったCSPの説明

CSPの動作を理解するための簡単なWEBアプリケーションをnode.jsで作成しました。ソースはこちらからダウンロードいただけます。

このアプリケーションでは同じhtmlをCSPヘッダーのみ変更してレスポンスを返すものです。

▲左:CSPヘッダー無し or 適切に設定 右:CSP 全部制限 (default-src ‘none’)

ここでは説明のため、CSPヘッダーにdefault-src ‘none’を設定して全リソースの読み込み・実行を拒否してみました。画像のとおり、CSP違反となるものはHTMLとしてタグが記載されていてもブラウザ側で読み込みや実行を行わないよう制御してくれます。

CSPヘッダーを適切に設定することで、意図しているリソース・スクリプトのみを読み込み、意図しないものや何らかの方法でインジェクションされたタグのリソースは読み込まないようにすることが可能です。

サンプルのWEBアプリケーションではswitch文内でCSPヘッダーを指定しています。Content-Security-Policyヘッダーの指定以外はまったく同じです。

javascript

switch (req.url) {
    // CSPヘッダーなし
    case '/no-csp':
        header = {
            'Content-Type': 'text/html; charset=UTF-8',
        };
        body = fs.readFileSync('view/content.html');
        break;

    // CSP 全部制限
    case '/csp-none':
        header = {
            'Content-Type': 'text/html; charset=UTF-8',
            'Content-Security-Policy': "default-src 'none'",
        };
        body = fs.readFileSync('view/content.html');
        break;

    // 省略
}

 
CSP違反はChromeのDevToolsのConsoleタブで確認できます。

サンプルのWEBアプリケーションで適切にCSPヘッダーを設定すると下記の通りになります(視認性を上げのため空白と改行を入れていますが、実際は1行で記述します)。

Content-Security-Policy-Report-Only: 
    base-uri 'self';
    default-src 'self';
    img-src 'self' *.openstreetmap.org;
    frame-src https://www.openstreetmap.org;
    style-src 'self' https://cdn.jsdelivr.net 'nonce-CFCgF+c44it7V6VMwOgK/w==';
    style-src-attr 'unsafe-inline';
    script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com 'strict-dynamic' 'nonce-<ランダムな値>'

 

該当箇所のコード(クリックで開きます)

javascript

switch (req.url) {
    // 省略

    case '/csp-proper':
        header = {
            'Content-Type': 'text/html; charset=UTF-8',
            'Content-Security-Policy': [
                // CSPの各ディレクティブ
                `base-uri 'self'`,
                `default-src 'self'`,
                `img-src 'self' *.openstreetmap.org`,
                `frame-src https://www.openstreetmap.org`,
                `style-src 'self' https://cdn.jsdelivr.net 'nonce-${nonceValue}'`,
                `style-src-attr 'unsafe-inline'`,

                // 'self'およびドメインのホワイトリストは'strict-dynamic'指定時は無視されるが、CSP Level3に未対応の場合はフォールバックされて参照される
                `script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com 'strict-dynamic' 'nonce-${nonceValue}'`,
            ].join('; '),
        };
        body = fs.readFileSync('view/content.html')
            .toString()
            .replaceAll('${nonceValue}', nonceValue);
        break;

    // 省略
}
ディレクティブ 説明
base-uri とりあえず同一オリジンは許可。
img-src 画像の参照先を制限(ホワイトリスト)。
frame-src iframe等の参照先を制限(ホワイトリスト)。
style-src スタイルシートの参照先を制限(ホワイトリスト)。
インラインスタイルはnonceが一致するもののみ許可。
style-src-attr ‘unsafe-inline’で各タグのstyle属性の指定を許可
script-src nonceが一致するものおよびそのルートスクリプトによって読み込まれるスクリプトのみ許可(CSP level3の場合)。
ホワイトリストおよびnonceが一致するもののみ許可(CSP Level2の場合)
default-src 上記以外のリソースは同一オリジンのみ許可

script-srcについて少し補足ですが、CSP Level3で追加されたstrict-dynamicにブラウザが対応しているかどうかで動作が異なります。CSP Level3に対応していないブラウザをサポートする場合は、両対応できるような記述が必要です。

strict-dynamicに対応しているブラウザでstrict-dynamicが指定されると’self’やホワイトリストドメインが無視されます。そのため、以下と同等のポリシーになります。

script-src 'strict-dynamic' 'nonce-<ランダムな値>',

 
一方、strict-dynamicに対応しているブラウザの場合、strict-dynamicが無視され代わりに’self’やホワイトリストドメインが参照されます。よって、以下と同等のポリシーになります。

script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com 'nonce-<ランダムな値>',

CSP対応を求められたら

まずは費用対効果を考慮して対応が本当に必要かを決めましょう。「脆弱性診断に引っ掛かったから」というだけで100%対応すると大変な上に効果はあまり効果的でない場合があります。

まずはベンダーにも相談し、対応しない場合のリスクと他サービスでの対応状況を把握したうえで、導入を検討することをおすすめします。

もちろんCSPを設定するに越したことはないですが、動的コンテンツがなく外部リソースを利用していない場合、早急にCSP対応をする必要性はあまり高くありません。ユーザーの入力がある程度そのまま表示されるようなサービス(SNS, ブログなど)では、設定することでXSSのリスクを大きく軽減できるので導入しておくのがベターでしょう。

ちなみに大手の著名なサイトでは対応されていますが、現状では多くのサイトが未対応またはレポートのみ対応となっています。

【参考】CSP対応サイト(クリックで開きます)

※以下のサイトはReport-Onlyのみも含んでいます
※表は上下・左右に動きます

サイトURL CSP
https://www.google.com/search?q=検索ワード Content-Security-Policy:
object-src ‘none’;
base-uri ‘self’;
script-src ‘nonce-39Lyyc_mYyO6GPfLzSMTqw’ ‘strict-dynamic’ ‘report-sample’ ‘unsafe-eval’ ‘unsafe-inline’ https: http:;
report-uri https://csp.withgoogle.com/csp/gws/cdt1 ;
https://www.amazon.co.jp/
Content-Security-Policy:
upgrade-insecure-requests;
report-uri https://metrics.media-amazon.com/ ;
https://zenn.dev/
Content-Security-Policy:
script-src 'self' https: 'strict-dynamic' 'nonce-lHCuMbOUGQf1MaPvhEKy/Vn/nAEJLi1Wr/MPRA12v4s=' https://embed.zenn.studio/js/listen-embed-event.js http://www.googletagmanager.com https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js 'sha256-VBc9tzS+U9SzON+C7B2ZnWQcfbc8HELDISqMEIhELLs=' 'sha256-N9YIN+P8sTmLT0uJPtGBiAASdpoJJYKfyJRjXSwTXys=' 'sha256-KNm0/xK1a/MUS2W6s/HYNdp8BjyrUCkD+qMikvBKNtE=';
object-src 'none';
base-uri 'none';
report-uri https://asia-northeast1-zenn-dev-production.cloudfunctions.net/csp-logger ;
https://inside.pixiv.blog/(レポートのみ)
Content-Security-Policy-Report-Only:
block-all-mixed-content;
report-uri https://blog.hatena.ne.jp/api/csp_report ;
https://twitter.com/home Content-Security-Policy:
connect-src ‘self’ blob: https://*.pscp.tv https://*.video.pscp.tv https://*.twimg.com (その他たくさん) ;
default-src ‘self’;
form-action ‘self’ https://twitter.com https://*.twitter.com;
font-src ‘self’ https://*.twimg.com;
frame-src ‘self’ https://twitter.com https://mobile.twitter.com https://pay.twitter.com (その他たくさん);
img-src ‘self’ blob: data: https://*.cdn.twitter.com https://ton.twitter.com https://*.twimg.com (その他たくさん);
manifest-src ‘self’;
media-src ‘self’ blob: https://twitter.com https://*.twimg.com https://*.vine.co https://*.pscp.tv (その他たくさん);
object-src ‘none’;
script-src ‘self’ ‘unsafe-inline’ https://*.twimg.com https://recaptcha.net/recaptcha/ (その他たくさん);
style-src ‘self’ ‘unsafe-inline’ https://accounts.google.com/gsi/style https://*.twimg.com;
worker-src ‘self’ blob:;
report-uri https://twitter.com/i/csp_report?a=O5RXE%3D%3D%3D&ro=false ;
https://www.facebook.com/ Content-Security-Policy:
default-src data: blob: ‘self’ https://*.fbsbx.com *.facebook.com *.fbcdn.net ‘wasm-unsafe-eval’;
script-src *.facebook.com *.fbcdn.net *.facebook.net *.google-analytics.com *.google.com 127.0.0.1:* blob: data: ‘self’ connect.facebook.net ‘wasm-unsafe-eval’;
style-src fonts.googleapis.com *.fbcdn.net data: *.facebook.com ‘unsafe-inline’;
connect-src *.facebook.com facebook.com *.fbcdn.net *.facebook.net wss://*.facebook.com:* (その他たくさん);
font-src data: *.gstatic.com *.facebook.com *.fbcdn.net *.fbsbx.com;
img-src *.fbcdn.net *.facebook.com data: https://*.fbsbx.com *.tenor.co (その他たくさん);
media-src *.cdninstagram.com blob: *.fbcdn.net *.fbsbx.com www.facebook.com *.facebook.com https://*.giphy.com data:;
frame-src *.doubleclick.net *.google.com *.facebook.com www.googleadservices.com *.fbsbx.com fbsbx.com data: www.instagram.com *.fbcdn.net https://paywithmybank.com https://sandbox.paywithmybank.com ;
worker-src *.facebook.com/static_resources/webworker_v1/init_script/ *.facebook.com/static_resources/webworker/init_script/ *.facebook.com/static_resources/sharedworker/init_script/ *.facebook.com/static_resources/webworker/map_libre/ *.facebook.com/sw/ *.facebook.com/sw;
block-all-mixed-content;
upgrade-insecure-requests;

Content-Security-Policy-Report-Only:
default-src data: blob: ‘self’ https://*.fbsbx.com *.facebook.com *.fbcdn.net ‘wasm-unsafe-eval’;
script-src *.facebook.com *.fbcdn.net *.facebook.net *.google-analytics.com *.google.com 127.0.0.1:* blob: data: ‘self’ connect.facebook.net ‘wasm-unsafe-eval’;
style-src fonts.googleapis.com *.fbcdn.net data: *.facebook.com ‘unsafe-inline’;
connect-src *.facebook.com facebook.com *.fbcdn.net *.facebook.net wss://*.facebook.com:* (その他たくさん);
font-src data: *.gstatic.com *.facebook.com *.fbcdn.net *.fbsbx.com;
img-src *.fbcdn.net *.facebook.com data: https://*.fbsbx.com *.tenor.co (その他たくさん);
media-src *.cdninstagram.com blob: *.fbcdn.net *.fbsbx.com www.facebook.com *.facebook.com https://*.giphy.com data:;
frame-src *.doubleclick.net *.google.com *.facebook.com www.googleadservices.com *.fbsbx.com fbsbx.com data: www.instagram.com *.fbcdn.net https://paywithmybank.com https://sandbox.paywithmybank.com ;
worker-src *.facebook.com/static_resources/webworker_v1/init_script/ *.facebook.com/static_resources/webworker/init_script/ *.facebook.com/static_resources/sharedworker/init_script/ *.facebook.com/static_resources/webworker/map_libre/ *.facebook.com/sw/ *.facebook.com/sw;
block-all-mixed-content;
report-uri https://www.facebook.com/csp/reporting/?minimize=0 ;

既存のWEBサイトで手軽にCSP適用時の動作を確認する方法

CSPの適用と確認を行うには、Content-Security-Policyヘッダーを設定した上で対象Webサイトにアクセスして確認するか、アクセス後に送られてきたレポートを確認する必要がありました。

しかしChrome 113よりDevToolsでHTTPヘッダーを上書きする機能が導入され、気軽にCSPの導入を試すことができるようになりました。この機能の詳細については、Google公式(What’s new in DevTools Chrome 113)の情報を参照してください。

CSPヘッダーの追加・変更方法

以下の4ステップでおこないます。

  1. Sources -> Overrides タブで Select folder for overrides を押し、上書きヘッダーなどの情報を保存するディレクトリを選択します。フォルダの場所はどこでも問題ありません。
  2. 対象のWebページでDevToolsのNetworkタブを開きます。
  3. CSP書き換え対象のリクエスト/レスポンスのHeaderタブを開き、Response Headersセクション下部にあるAdd headerボタンを押します。
  4. 追加したいヘッダーを入力します。

.headerファイルには上書きするHTTPヘッダーの情報が格納されています。DevToolsではなく.headerファイルを直接編集しても反映されます。

この操作で設定した追加のHTTPヘッダーはDevToolsを開いてる かつ Sources -> Overrides タブで Enable Local Overridesにチェックが入っている場合の上書きされます。

試しにLIGのWEBサイトでContent-Security-Policy: media-src ‘none’と追加してみたところ、背景の動画が読み込まれなくなってしまいました。

追加したHTTPヘッダーの削除

DevToolsのSources -> Overrides タブの Enable Local Overridesの右側にクリアボタンがあります。このボタンを削除すると上書きが解除されます。

.headerファイルなどはPC内に残ったままなので、不要であれば手動で削除しましょう。

まとめ

CSP対応をしたほうがベターではありますが、現状では未対応のサイトも多く、対応が必要かはケースバイケースです。SNSやブログのようにユーザーの入力がある程度そのまま表示されるようなサービスなど、高いセキュリティ(XSS対策)が求められる場合は対応を検討した方がよいと思います。

CSP(特にnonce)に対応できない場合は、対応しない場合のリスクと別案(バリデーション・サニタイズなどで脅威を排除する)もしっかりと把握したうえで、リスク管理を徹底するようにしましょう。

最新情報をメルマガでお届けします!

LIGブログではAIやアプリ・システム開発など、テクノロジーに関するお役立ち記事をお届けするメルマガを配信しています。

<お届けするテーマ>
  • 開発プロジェクトを円滑に進めるためのTIPS
  • エンジニアの生産性が上がった取り組み事例
  • 現場メンバーが生成AIを使ってみた
  • 開発ツールの使い方や開発事例の解説
  • AIをテーマにしたセミナーの案内
  • 最新のAI関連ニュースまとめ など

「AIに関する最新情報を集めたい!」「開発ツールの解説や現場の取り組みを知りたい!」とお考えの方は、ぜひお気軽に無料のメルマガをご購読くださいませ。

購読する(無料)

この記事のシェア数

大学卒業後にゲーム会社に入社しWEBのバックエンドエンジニアおよびインフラエンジニアとして従事。数社を渡り歩いた後にテクニカルディレクターとしてLIGに入社。

このメンバーの記事をもっと読む