PHP
HTTP
Security
header
CSP

WEBページをコンテントセキュリティポリシー(CSP)対応させる

自社のWEBサービスをCSP対応させたかった話です。

そもそもCSPって何?

WEBページの潜在的なXSSなどに対する脆弱性を緩和するための仕組みです。CSPに対応させるというのはCSPの仕組みをもつブラウザの機能を利用するために、レスポンスヘッダーを追記することを指します。具体的にはヘッダーにWEBページが必要とするリソースを列挙する作業になります。そうするとブラウザはそれらのリソース以外を読み込みません。

簡単に言えば、うちのページはここに挙げた場所からしかリソースを持って来ないつもりだし、もし違うところから持って来てたらそいつら全部遮断してしまって構わんよとブラウザに宣言することです。

こうすればXSSなどの意図しないスクリプトの実行を防げるわけですね。同時に自分で自分の首を絞める行為でもあります。

PHPで実装してみる

PHPプログラムの場合、CSPの書き方は以下の三つがあります。

  1. PHPのheader関数で送信する
  2. WEBサーバー(apache, nginx)の設定ファイルに書く
  3. htmlヘッダーのmetaタグに書く(後述のreport_uriが設定できない)

1で実装する場合、以下のcontent-security-policy: ...の部分をheader関数に入れて

header("Content-Security-Policy: ...");

とやるだけです。

CSPヘッダーの構造

content-security-policy: default-src <source>; script-src <source> <source>; ...

上記のようにcontent-security-policyヘッダーの後にポリシーディレクティブをセミコロン区切りで書き連ねます。これにより、ポリシーディレクティブに記述のないリソースはシャットアウトされてしまいますが、替わりにcontent-security-policy-report-onlyヘッダーを使えば、そのエラー内容を特定のURLにPOSTするだけで、読み込みは行ってくれます。いきなり適用するのではなく、実験的にCSPを導入するのに便利な機能です。

Fetchディレクティブ

上の例でいうdefault-srcとかscript-srcの部分です。リソースの種類ごとにソースを指定します。例えば

default-src 'self'; script-src 'self' https://www.google-analytics.com

と書けば、同じURLであるかhttps://www.google-analytics.comにあるjavascriptはロードし、かつそれ以外のリソース(スタイルシートやフォント、画像など)は同じURLにあるもののみロードする、という意味になります。このようにdefault-srcに書いたリソースは全てのディレクティブに適用されるのではなく、特に記述のないディレクティブに対し有効であることに注意が必要です。ちなみによくある以下のソースを読み込むにはCSPはここまで書かないといけません(全部default-srcに入れてもいいですが)。

  • Google Analytics
  • Google Adsense
  • Google Tag Manager
  • Facebook のウィジェット
  • FontAwesome
content-security-policy-report-only:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://connect.facebook.net https://www.google-analytics.com https://www.googletagmanager.com https://stats.g.doubleclick.net blob: data:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://use.fontawesome.com https://netdna.bootstrapcdn.com use.fontawesome.com;
img-src * data:;
font-src 'self' https://fonts.gstatic.com https://use.fontawesome.com https://netdna.bootstrapcdn.com data:;
frame-src 'self' https://www.facebook.com https://staticxx.facebook.com https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com https://stats.g.doubleclick.net https://api.ipify.org;
report-uri <report先URL>

見やすくするために改行しています。report-onlyで実装して見てエラーが出たら修正の繰り返しです。slackにエラー通知を飛ばしたのですが、通知欄が凄まじいことになるのでオススメしません。

以下、このソースに関する説明(弁明)になります。

report-uriについて

content-security-policy:の代わりにcontent-security-policy-report-only:ヘッダーを使うとき、エラーレポートを飛ばす先を指定します。以下のような形式のデータが飛んできます。

{
    "csp-report":{
        "blocked-uri":<ブロックされたソース>,
        "document-uri":<送信元URL>,
        "original-policy":<設定したCSP>,
        "referrer":<リファラー>,
        "violated-directive":<ブロックの原因となったFetchディレクティブ>
    }
}

これをPHPサーバーで受けたかったら

$payload = file_get_contents('php://input');

で変数に代入できるので、あとは煮るなり焼くなりslackAPIで通知飛ばすなり好きにできます。

ワイルドカード*について

img-src * data:;という記述、不思議ですがこれはワイルドカードがblob:data:filesystem:といったスキーマのURIを含まないからです。

dataスキーマについて

上の例であるdata:の部分です。これはdata:形式のURIを受け付ける記述で、Mozillaのドキュメントでは

コンテンツのソースとして data: の URI を使うことができるようにします。これは安全ではありません。攻撃者は任意の data: URI を挿入することもできます。使用は控え、スクリプトには絶対に使用しないでください。

とありますが、これを入れないと大概うまく動きません。仕方ないよね。

script-src'unsafe-inline'入れていいのか問題

ダメです。'unsafe-inline'と書けばスクリプトなどのインライン記述を許すわけですが、これはインラインスクリプトによる攻撃などを防ぐというCSPの目的の一つを崩壊させています。なんのためのCSPやねんと。でも実際の運用ではなかなか厳しんですよと。じゃあ入れざるを得ないんよと。

【追加】Mixed Content 警告の防止

HTTPS のページに HTTP で読み込むリソースがあったら出る、あの警告です。これの防止策は二つあって

  • httpと書かれていてもhttpsでアクセスしに行く upgrade-insecure-requests
  • httpのリソースはそもそも読み込まない block-all-mixed-content

これらのうちどちらかを CSP ヘッダーに記述します。前者の方が良さげ。

感想

以上、散々な結果ですがやはり実際の運用を考えると、完全にセキュアな書き方は難しいです。今後CSP対応が必須になって来たときに、もう少しやりやすい感じになって欲しいですね。

あとなぜか頻繁にwebviewprogressproxy://をブロックした旨の通知が来るので気になっているのですが、このスキーマはwebviewで用いられるようで、基本的にブロックしてしまっていいそうです。ソースはDropboxの記事

参考

Content-Security-Policy - HTTP | MDN
[CSP] On Reporting and Filtering | Dropbox Tech Blog