Content Security Policy でユーザーを守ろう

はじめに

こんにちは、ビットバンクのチーフ・ビットコイン・オフィサーのジョナサンです。

最近仮想通貨以外のセキュリティー面を色々と調査しておりましたが、その中でも一番注目している Content Security Policy (CSP) についてお話できればと思っています。

仮想通貨を扱うことのありそうなウェブサービスを運営される皆さんにも是非検討していただきたいものです。

Content Security Policy (CSP)は誰を守る?

CSPの仕組みから説明すると分かりやすいと思います。CSPが活用される時、以下のような流れになります。

  • ウェブサービスはHTTPレスポンスのヘッダ(若しくはタグ)にて Content-Security-Policy のヘッダを返す
  • 利用者のブラウザがそのヘッダを見ると、ヘッダに記載されたルールに従いCSPを有効化する
    • 例: default-src 'self'; img-src *; script-src 'self' 'nonce-ZHNmNjc1R0Q3OGhq' 'sha256-B2yPHKaXO7sBhuChIbabYmUBFZdVfKKXHbWtWidDVF8='; report-uri https://example.com/cspreports
    • 画像ファイルを読み込む <img> タグはどこからとってきてもOK
    • javascriptのjsファイルは同ページのドメイン、若しくは <script nonce="..."> のように inline script タグの中にnonceを指定している、若しくは inline script タグの中のjavascriptをsha256に掛けたら、上記のハッシュ値になるものだけ実行してOK
    • もし違反が発生したら、example.com/cspreportsに対してPOSTで報告を送る
    • <img> <script>以外のものは指定がルールに無いので、 default-src に従い、ページと同ドメインのリソース以外を禁止
  • 例えば、利用者が悪意のある拡張機能をブラウザにインストールしたとして (騙されたなど)、その拡張が悪意のあるjavascriptをページに勝手に挿入し、ユーザーのキー押下などを記録して外部に送信していたとする。
    • Ajaxなどで外部に接続しようとすると、 connect-src (default-src) に阻止され、ブラウザが守る
    • 外部サーバーからCSSファイルを読み込もうと見せかけ、その嘘のCSSファイルの名前があなたのキーログの内容をbase64エンコードしたものだった場合、それも阻止 (style-src)
    • ただし、上記の例では、<img>は何でも良い設定になっているので、 キーログ内容.jpgを読み込めば、攻撃者がHTTPリクエストのログを見るだけであなたのキー押下履歴が見れちゃう

上記のように、「サービス運営者はこれと、これと、あれが必要だ、それ以外は要らないと言っている(上記のルール記載)」とブラウザが認識し、そのルールに従って、サイトに訪れる一人ひとりの利用者を守っています。

ルールを決めるための directive の管理

ルールを決める際、外部から読み込むものがありすぎて、何が必要かというルールが作れないサービスは少なくないはずです。 (CSPはこういうインターネットビジネスあるあるを払拭するために作られた)

先ずはルールのシンタックスを理解することが大切です。

主要な directive のグループ

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

# よく使うもの
default-src (下記のようなdirectiveが無い場合、これに従う)
script-src (<script>)
style-src (<style> <link rel="stylesheet">)
font-src (CSS内の url() などで読み込んだフォント)
frame-src (<frame> <iframe>)
img-src (<img>)
connect-src (javascript内で外部に送受信するXHR(fetch, ajax, jQueryなど))

# おすすめ (これらの後は domain など指定しない)
block-all-mixed-content
upgrade-insecure-requests

protocol/domain directive

directiveグループを指定した後に、半角スペース区切りでドメイン、プロトコルなどを指定し、セミコロンで終わらせます。

例:

script-src 'self' 'unsafe-inline' 'unsafe-eval' example.com data: wss: https://sub.example.com;
  • javascriptにおいて
    • ページと同ドメインはOK
    • HTML内の直接書いた<script>タグは全部OK (XSSに脆弱な掲示板のalert()試しが通ってしまう)
    • javascript内で eval() を使って文字列を実行するjavascriptに変換しても良い
    • http://example.comhttps://example.com はOKだが、 サブドメインはNG
    • data: プロトコルで指定したソースは全部OK
    • wss: プロトコルで指定した接続は全部OK
    • https://sub.example.com はOKだが、 http://sub.example.com はNG

なお、'none'という爆弾もあるが、これを使ってしまうと、javascriptが全く実行させてくれません。static HTMLサイトでJSゼロのサイト今時少ないと思いますが、一応使えます。

nonce directive

デフォルトでは、CSPを有効にすると、HTMLファイルに直接書いたjavascriptや他のJSがDOMを直接編集して挿入するもの (Google Tag Managerを凝視w) は全部ダメです。かといって、'unsafe-inline'はあまり使いたくありません。安全にinlineのJSを活用するための一つのやり方は nonce directive

  1. サーバー側で乱数を取得し、base64エンコード掛ける、これがnonce
  2. サーバーが返すヘッダORタグのCSPの中に 'nonce-<base64乱数>' を挿入
  3. サーバー側でHTMLを返す時のテンプレートの中に、どうしてもinlineじゃないといけないJSのタグのnonce属性を挿入 <script ... nonce="<base64乱数>" ...> ... </script>

ワイルドカード使えるが、ドメイン部分の一番左の一箇所のみ。 *.example.com https://*.example.com は OK だが、 www.google.* はNG

欠点は、サーバー側が動的に返すHTMLを変えられないとか、CDNのキャッシュに頼りたいというサービスには不向きなところです。

hash directive

便利ツール: https://report-uri.com/home/hash
(ここにjs/cssのURLを入れてボタン押すと、そのJSファイルのハッシュディレクティブを教えてくれる)

inlineのJSは永遠に変えない、でも何故か別ファイルにできない理由があるなら、ハッシュ値指定にすれば良いかもしれません。

シンプルです。<script>alert('Hello, world.');</script>開けるタグと閉じるタグの間の文字全てをハッシュに掛けてbase64エンコードします。(前後の空白をtrimしない)

hash directive 使うとSRIと同じ仕組みです。

一般的な導入方法

いきなり本番に投下すると、サイトが色んなブラウザの過保護によって壊れまくります。なので、CSPの導入は大体③つのフェーズで構成されています。

  1. Report Onlyで情報収集: 穴開けまくってはreportの集計を監視
  2. 穴たくさん空いた状態でもCSP導入: reportを引き続き監視
  3. ウェブアプリ側をどんどん改修して穴の必要性を一つ一つ無くし、穴を塞いでいく

Report Only

(現時点でReport Onlyをタグでサポートしているブラウザが無い)
上記CSPを Content-Security-Policy-Report-Only ヘッダに入れると、実際のブロックは行わないで、あくまでルール違反が発生した時はレポートのみ出すようになります。

現在report-uriのdirectiveにて報告先のエンドポイントが指定できます。(今後report-uriが廃止されreport-toに変わるらしいが、ブラウザによる)

では、POSTで報告が挙がってきたとき、それをどうすればいいのかというと、集計したり追えば良いのです。面倒くさいという人は https://report-uri.com がおすすめ。CSPの報告を監視するためのサービスになります。無料アカウントの枠内で結構なレポート数がカバーできるので是非試してみましょう。

sentryなどもCSPの受け皿的なものもあったりします。

reportは大体こんな形をしています。

{
	"csp-report": {
		"document-uri": "https://example.com/foo/bar",
		"referrer": "https://www.google.com/",
		"violated-directive": "default-src self",
		"original-policy": "default-src self; report-uri /csp-hotline.php",
		"blocked-uri": "http://evilhackerscripts.com"
	}
}

violated-directiveblocked-uriを見れば、大体把握できますが、ブラウザによっては送られるJSONが少し異なります。

  • 平均的なDAUを超える特定のレポートは「みんなが必ず取得している」系なので、穴を開けてあげよう。
  • 1日に数回しか来ないものはどこかの拡張機能が変な仕組みになっていて外部リソースをDOMの中からとりに行っているとかのはず
  • googleのドメインの数の多さにびっくりするし、ワイルドカード使えるように国ごとのアナリティクスをgoogle.comのサブドメインにして欲しさが半端ない
  • 各種Analyticsとの戦いになることが殆ど

CSP 導入

導入すれば、本番リリース時はCSPを意識しないと痛い目に遭う!
SEO担当が技術者と一切相談無しにtag managerのタグ編集したりも容易にできなくなる!
開発環境があれば、できれば開発環境でも似たようなCSPを導入することが大事!

大量のユーザーがいきなり同じレポート挙げてくることでマルウェアが自社利用者の中で普及していることが分かったりします。 (https://gigazine.net/news/20180705-fortnite-cheat-tool-malware/)

ウェブアプリ改修で穴を塞いでいく

  • 独自開発アナリティクスが開発できれば、一番理想的 (Analyticsと戦うのが辛い)
  • 外部サイトから取得するものがバージョン変わらないなら、自社ホスティングすることも検討すべき
  • アプリ内で eval() や 動的にDOMに<script>挿入したりしていればそれをやめること

問題が発生しやすい理由 (CSPが何を壊す?)

Analytics

CSPを検証していると、SEO系のサービス(世界のGoogleでさえ)がやっていることのセキュリティーの悪さが目立ちます。

今まではGoogle Tag ManagerでSEO担当が色々勝手にいじれたのが、CSP導入すると、穴開けてあげないとダメになったりします。

ブラウザ拡張機能

仕組み次第なところもありますが、悪意の無い便利なブラウザ拡張機能が壊れたりします。これは、悪意が無いけど、他にやり方が思い浮かばなくてセキュアじゃないやり方でやってしまっていることが多いです。

フロント系ライブラリ

Angular, reactなどなどとCSPの相性をよくするための動きがわりかし最近本格的に動いています。そのため、ずっと前から実装してあるフロント系のJSのせいでCSPの穴をたくさん必要とします。

おわりに

最初は穴開けまくりで'unsafe-inline' 'unsafe-eval'など使ったとしても、掛けないよりはCSP掛けた方が利用者のためになります。

利用者がたくさんいるサービスに途中から導入するのは大変で時間がかかることですが、是非皆さんも導入してみて下さい。

以上、ジョナサンでした。

おまけ

nodeJSでCSPのバージョン管理苦労していたら考えついた裏ワザをどうぞ。
改行で項目を分けているのでバージョン管理しやすく、楽です。
テンプレート文字列最高です。

const CSP_DIRECTIVES = `

default-src
'self'
;
style-src
'self'
;
report-uri
https://sentry.io/api/xxxxx/security/?sentry_key=yyyyy&sentry_environment=prod&sentry_release=csp002

`.trim().replace(/\n/g, ' ').replace(/ ;/g, ';')
expand_less