Firefox 60で修正されたContent Security Policy(CSP)のstrict-dynamicをバイパスできた脆弱性について書きます。
https://www.mozilla.org/en-US/security/advisories/mfsa2018-11/#CVE-2018-5175
A mechanism to bypass Content Security Policy (CSP) protections on sites that have a script-src policy of 'strict-dynamic'. If a target website contains an HTML injection flaw an attacker could inject a reference to a copy of the require.js library that is part of Firefox’s Developer Tools, and then use a known technique using that library to bypass the CSP restrictions on executing injected scripts.
strict-dynamicとは
まず簡単にCSPについておさらいしながら、strict-dynamicが誕生した背景を少し書いておきます。
従来のCSPは、ドメインをホワイトリストする形でリソースのロードを制限します。
例えば、次のCSP設定は、自分自身のオリジンと trusted.example.com からのみ、JavaScriptのロードを許可します。
Content-Security-Policy: script-src 'self' trusted.example.com
これで、 XSSがあった場合にも、evil.example.org からスクリプトをロードされたり、インラインのスクリプトを実行されることはなくなります。十分安全に思えますが、trusted.example.com にCSPのバイパスに都合の良いスクリプトが置かれている場合などに、まだ悪意あるスクリプトを実行される可能性が残っています。具体的には、trusted.example.com に JSONP がある場合などです。
以下は、このCSP設定の下で、完全にロードが許可されるスクリプトです。
<script src="//trusted.example.com/jsonp?callback=alert(1)//"></script>
このAPIがcallbackパラメータに渡された入力をcallback関数部分に直接反映するものであれば、以下のように、任意のスクリプトとして利用できてしまいます。
alert(1)//({});
その他にも、AngularJSがロードできるとバイパスが起こることが知られています。特に、CDNなどの、多くのJavaScriptをホストしているドメインを許可した場合には、この可能性がより現実的になります。
このように、ホワイトリストでは、CSPを安全に運用することが難しい場合があります。そこで考えられたのがstrict-dynamicです。このオプションを次のように設定したとします。
Content-Security-Policy: script-src 'nonce-secret' 'strict-dynamic'
すると、ホワイトリストは無効化され、nonce属性がsecretという値を含んだスクリプトのみがロードされるようになります。
<!-- これはロードされる -->
<script src="//example.com/assets/A.js" nonce="secret"></script>
<!-- これはロードされない -->
<script src="//example.com/assets/B.js"></script>
このとき、A.jsがさらに別のJavaScriptをロードして使いたい場合も考えられます。こういった場合、特定の条件でロードされれば、nonceをつけなくてもロードが許可されるようになっています。仕様で書かれている言葉を使うと、non-"parser-inserted" なスクリプト要素は、スクリプトを使ってロードすることが許可されます。
どのようなものが許可されるか、以下に具体例を示します。
/* A.jsのコード */
//これはロードされる
var script=document.createElement('script');
script.src='//example.org/dependency.js';
document.body.appendChild(script);
//これはロードされない
document.write("<scr"+"ipt src='//example.org/dependency.js'></scr"+"ipt>");
createElement()
などを使ってロードする場合が、non-"parser-inserted"なスクリプト要素となり、ロードが許可されます。一方で、document.write()
などを使って書き出す場合は、parser-inserted なスクリプト要素となり許可されません。
ここまで、strict-dynamicの動作をおおまかに説明しました。
より詳しくは仕様を参照してください。
さて、strict-dynamicを使えば、全くバイパス不可能かというとそうでもありません。
次に、strict-dynamicの既知のバイパス手法について紹介します。
既知のstrict-dynamicバイパス
ターゲットのページで特定のライブラリが使われている場合に、strict-dynamicもバイパス可能であることが知られています。
GoogleのSebastian Lekies氏、Eduardo Vela Nava氏、Krzysztof Kotowicz氏によって、影響を受けるライブラリがまとめられています。strict-dynamicのバイパスだけではなく、ライブラリから生じるその他のCSPのバイパスもまとめられています。
この中の、require.js のstrict-dynamicのバイパスをみてみましょう。
ターゲットのページが、strict-dynamicを設定したCSPを持ち、require.jsをロードしており、シンプルなXSSを持っているとします。このとき、次のようなスクリプト要素が挿入されると、nonceを知らなくても、任意のスクリプトを実行できてしまいます。
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<!-- XSS END -->
<script nonce="secret" src="require.js"></script>
require.jsは、data-main属性を持ったscript要素をみつけると、以下と同等のコードから、新たなスクリプトをロードするようになっています。
var node = document.createElement('script');前述したように、 strict-dynamicが設定されているページにおいて、
node.src = 'data:,alert(1)';
document.head.appendChild(node);
createElement()
からスクリプトをロードする場合は、nonceがなくてもロードが許可されます。このように、既にロードされているJavaScriptのコードの動作を使って、場合によってはstrict-dynamicもバイパスできます。今回のFirefoxの脆弱性は、このrequire.jsの動作のおかげで発生していました。
本題のFirefoxの脆弱性について解説していきます。
Firefoxでstrict-dynamicバイパス(CVE-2018-5175)
Firefoxは、一部のブラウザの機能をレガシーな拡張機能を使って実装しています。レガシーな拡張機能とは、WebExtensionsではなく、FirefoxがFirefox 57でサポートを廃止したXUL/XPCOMベースの拡張機能のことです。現在最新のFirefox 60でも、ブラウザ内部ではまだこの仕組みを使っている部分があります。
このバイパスでは、ブラウザ内部で使われている、レガシーな拡張機能のリソースを使います。WebExtensionsでは、マニフェストの web_accessible_resources で指定した拡張機能のリソースは、任意のウェブページからロードできるように設定できます。これと同じように、レガシーな拡張機能のマニフェストにも、contentaccessible というオプションがあり、任意のウェブページから拡張機能のリソースのロードを許可できます。今回のバイパスでは、ブラウザ内部のリソースであるrequire.jsが、このオプションによって、ウェブアクセシブルな状態で公開されていたことで、バイパスに利用できてしまっていました。
マニフェストを見てみましょう。Windowsの64bitのFirefoxであれば、次のURLをFirefoxで開くことでマニフェストの内容を確認できます。
jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/chrome.manifest
content branding browser/content/branding/ contentaccessible=yes
content browser browser/content/browser/ contentaccessible=yes
skin browser classic/1.0 browser/skin/classic/browser/
skin communicator classic/1.0 browser/skin/classic/communicator/
content webide webide/content/
skin webide classic/1.0 webide/skin/
content devtools-shim devtools-shim/content/
content devtools devtools/content/
skin devtools classic/1.0 devtools/skin/
locale branding ja ja/locale/branding/
locale browser ja ja/locale/browser/
locale browser-region ja ja/locale/browser-region/
locale devtools ja ja/locale/ja/devtools/client/
locale devtools-shared ja ja/locale/ja/devtools/shared/
locale devtools-shim ja ja/locale/ja/devtools/shim/
locale pdf.js ja ja/locale/pdfviewer/
overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
overlay chrome://browser/content/places/places.xul chrome://browser/content/places/downloadsViewOverlay.xul
overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul
overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul
override chrome://global/content/license.html chrome://browser/content/license.html
override chrome://global/content/netError.xhtml chrome://browser/content/aboutNetError.xhtml
override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties
override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
override chrome://mozapps/locale/downloads/settingsChange.dtd chrome://browser/locale/downloads/settingsChange.dtd
resource search-plugins chrome://browser/locale/searchplugins/
resource usercontext-content browser/content/ contentaccessible=yes
resource pdf.js pdfjs/content/
resource devtools devtools/modules/devtools/
resource devtools-client-jsonview resource://devtools/client/jsonview/ contentaccessible=yes
resource devtools-client-shared resource://devtools/client/shared/ contentaccessible=yes
黄色い箇所が、問題のファイルを公開に設定している部分です。この2行は、resource: URIを作成するためにあります。最初の
resource devtools devtools/modules/devtools/
の行は、resource://devtools/ というURL に devtools/modules/devtools/ のディレクトリ( jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/devtools/modules/devtools/ にあるディレクトリ )をマッピングするという意味になります。これで、Firefoxから resource://devtools/ を開くことで、ディレクトリ以下のファイルへアクセスできるようになります。次の行も同様に、resource://devtools-client-jsonview/ へのマッピングを行っています。このURLが、contentaccessible=yesと設定されており、このディレクトリ以下に置かれたファイルを任意のページからロードできるようにしています。このディレクトリに問題のrequire.jsがあります。
require.jsを発見したら、あとはstrict-dynamicが設定されたページからロードするだけです。実際のバイパスは次のようになります。
https://vulnerabledoma.in/fx_csp_bypass_strict-dynamic.html
https://vulnerabledoma.in/fx_csp_bypass_strict-dynamic.html
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<script src="resource://devtools-client-jsonview/lib/require.js"></script>
<!-- XSS END -->
これで、data: URLがロードされ、アラートが実行されます。
あれ、strict-dynamicはnonceが必要だから、そもそもrequire.js自体ロードされないんじゃないか?と思うかもしれません。実は、どんなに厳しくCSPを設定しても、拡張機能のウェブアクセシブルなリソースはCSPを無視してロードされます。この動作はCSPの仕様でも触れられており、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].
Firefoxのresource: URIにもこの規則が適用されていました。このおかげで、利用者は、CSPが設定されているページでも、期待通りに拡張機能を動作させることができますが、一方で、今回のように、この特権をCSPのバイパスに利用される可能性も作ってしまっています。もちろん、この問題が起きるのはブラウザのリソースに限ったことではありません。一般の拡張機能でも、ウェブアクセシブルに設定されたリソースにバイパスに利用できるものがあれば、同じことが起きます。
今回は、ブラウザの内部リソースに問題のファイルがあったため、特定の拡張機能がインストールされているかどうかにかかわらず、デフォルトで全てのFirefoxでバイパスへの利用を許してしまっていました。修正後は、resource: URIにもCSPを適用するようになったようです。
今回は、ブラウザの内部リソースに問題のファイルがあったため、特定の拡張機能がインストールされているかどうかにかかわらず、デフォルトで全てのFirefoxでバイパスへの利用を許してしまっていました。修正後は、resource: URIにもCSPを適用するようになったようです。
終わりに
Firefoxの、CSPのstrict-dynamicをバイパスできた脆弱性について書きました。
ちなみにこの問題は、今年Cure53で出題したXSS Challengeの、Cure53 CNY Challenge 2018 の別解がないか探していた時に発見しました。こちらもstrict-dynamicをバイパスするチャレンジになっているので、興味がある方はご覧ください。
また、この別バージョンのXSS Challengeも現在出題中なので、ぜひトライしてみてください。
最後に、このバイパスに気付かせてくれたGoogleの研究に感謝します。