そろそろCSP Lv.2 nonceやろう

tl;dr

  • CSP Lv.2のnonceを使うと意外と簡単にCSPの恩恵を受けれるよ
  • Firefoxはunsafe-inlineとの挙動がおかしいので注意
  • サンプル実装としてExpressで簡単にnonce対応できるconnectプラグインを書いた(デモあり)
  • Violation Reportもブラウザによって細かい挙動の差異があるよ

CSP Lv.2 nonceの登場と背景

CSPの特にunsafe-inlineXSSに対して最終防衛線的に強力な効果がある。

しかし特にサーバーからの値の受け渡し部分などでどうしてもinline scriptを使いたくなるところがあり、unsafe-inlineを禁止するとDOM data等を使わざるを得ず、つらい感じだった。

ときは過ぎて、CSP Lv.2(当時はCSP 1.1と呼ばれていた)でnonceという便利設定の追加が検討されていることを知った。

例えばこういうHTTPヘッダを送ると、

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

script要素のnonce属性が一致するscript要素以外はブロックされる。

<script>
alert("Blocked because the policy doesn’t have 'unsafe-inline'.")
</script>

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
alert("Still blocked because nonce is wrong.")
</script>

<script nonce="Nc3n83cnSAd3wc3Sasdfn939hc3">
alert("Allowed because nonce is valid.")
</script>

これなら、nonce対応ブラウザに限定されるものの、現状の仕組みをほとんど変えずにCSPの恩恵に預かることができる。素晴らしい。*1

今年になってChromeFirefoxがデフォルトでnonceサポートが有効化されたので、それなりのユーザーシェアを救える下地が整ってきた。

nonce未対応ブラウザとの互換性

そんな折、tokuhiromさんがnonce良いという記事を公開。

ちょうど調べていたところだったので、nonce未対応ブラウザでもJSが動くようにunsafe-inlineも併記すると良いよ、としたり顔でコメントした。

仕様でもunsafe-inlineよりもnonceを優先するように書いてあるし、Chromeの挙動を確認してもそのとおりになっていた。CSP 1.0のみ対応しているブラウザとの後方互換のため、極めて正しい仕様だ。

If 'unsafe-inline' is not in the list of allowed script sources, or if at least one nonce-source or hash-source is present in the list of allowed script sources: ...

Content Security Policy Level 2 | 7.17 script-src

ところが、後日調査しているとFirefoxではこのフォールバックは機能せず、nonceとunsafe-inlineが両方あるとunsafe-inlineを優先してしまうことがわかった。

つまり、現状では 'unsafe-inline' 'nonce-xxxxxx'のように両方出力しているとChromeでしかinline scriptを禁止できず、かといってnonceだけの出力ではnonce非対応ブラウザではJavaScriptが全く動作しないしないという致命的な状況になるため、Firefoxユーザーにもnonceを適用するには結局サーバーサイドでuser-agentによるブラウザ判定をするしかない、ということになる。

やってくれたなー。

tokuhiromさんには後日勉強会で会ったときにごめんなさいしました。

nonceのサンプル実装

Express/Connectにはhelmetというセキュリティ対策全部載せみたいなミドルウェアパッケージがある。 そのCSP実装であるhelmet-cspはCSP対応が1.0止まりなので、これにnonce対応のコードを入れてみた。*2

READMEを見てもらえば分かるように、こんな感じで簡単にnonce対応できる。

app.use(csp({
    scriptSrc: "'self' 'nonce'",
    nonceFallback: true
}));
<!-- in your templates -->
<script nonce="{{cspNonce}}">alert('foo')</script>

nonceFallbackを有効にすると、上述したFirefox対応を自動でしてくれて、nonce対応済みのFirefox 31+の場合だけunsafe-inline無しのnonceを送信してくれる。

デモサイトも用意したので、遊んでみてください(ソース)。

CSP Violation Report

CSPでは設定に違反したらreport-uriで指定したURLにJSONをPOSTしてくれる便利機能がある。

またContent-Security-Policy-Report-Onlyヘッダを設定すると、違反したコードの実行をブロックはせずにレポートだけ送ってくれるので、本適用前に本番環境で影響を確認することができる。

で、このレポート周りをいじってたらブラウザごとの微妙な挙動の違いがあってハマったので知見共有。

Content-Type

"application/csp-report"というのはCSP Lv.2から定義されたらしいので、どちらの実装も間違ってはいない。 多くのWAFではapplication/csp-reportのリクエストをJSONにデフォルトでは変換してくれないので注意。

Cookieの送信

  • Chrome 38: 有り
  • Fireforx 34: 無し

レポートがいわゆるwith credentialかどうか。 report-uriが同一オリジンであっても、Firefox 34ではCookieが送信されなかった。 仕様にはどちらが正しいのか明記しているところは見つからなかった。

bodyのreport JSON

  • 仕様でMAYで定義されたプロパティの出力有無が結構異なる。
  • Firefoxでは仕様にない"script-sample"っていうのも出してきた。
  • Firefoxでは"blocked-uri":"self"だけどChromeでは空文字列。

この辺は実際見たほうが早いので、適当に作ったデモサイトをどうぞ。

このURLを開きながら、別タブでこれを開くと、最初のタブの方にリアルタイムにViolation ReportのJSONが表示されます。

ソースはこちら: https://github.com/teppeis/csp-report

まとめ

実運用の知見お願いします。

*1:対応ブラウザしか救えないの?という人がいるかもしれないけど、もともとCSP自体が対応ブラウザの被害を緩和するという性質の機能なのです。

*2:nonce対応のプルリクを投げる前に、元からあったbug fixのプルリクをマージしてもらって、次リファクタリングのプルリクを投げたところだけど、放置されてる。。