HTTP/2におけるHTTPヘッダインジェクション

前記事でHTTP/2の話が出たので、ちょっと脱線して「HTTP/2におけるHTTPヘッダインジェクション」に関連して行った実験について書く。

既に何年も前に語られているように(abend氏, yasulib氏)、HTTP/2では「ヘッダの区切り文字」という概念は無くなっているものの、HTTP/2を使うWebアプリケーションでもCR/LFを使ったヘッダインジェクションの可能性はある。

実験

HTTP/2のヘッダに改行を含めた不正な応答をサーバからブラウザに送り、ブラウザ側の挙動を調べる実験を行った。上述のようにHTTP/2では「区切り文字」という概念は無いため、改行等を入れた応答ヘッダを作れる。

今回の実験では、HTTP/2のヘッダの値に加えてヘッダ名についても改行を入れてみた。

結果は下のとおり。

  • Chrome/Safari/IE
    • エラー画面を表示した(期待通り)。
  • Firefox
    • ヘッダ値に含まれる改行をスペース(0x20)に変換する。
    • ヘッダ名に改行を入れることで、新たなヘッダを作り出せる。

ヘッダ名に改行を入れた例を下に挙げる。

aaa:aaa[0x0D][0x0A]set-cookie:hello=111;path=/;

サーバから上のようなHTTP/2応答のヘッダ名を受け取ると、Firefoxは改行の後のset-cookieを新たなヘッダとして解釈してしまった。FirefoxはHTTP/2のヘッダをhttp/1.1のテキスト形式にした上でparseをしているが、テキストにする際のチェックが漏れていたのだろう。

ちなみにFirefoxHttp2Compression.cpp内でヘッダをhttp/1.1のテキストにしているようだ。そんな処理をするのかと意外な感じがしたが、工数を減らすために同様の方式にしている実装は割とあるのかもしれない。

HTTPヘッダインジェクションの文脈で(ヘッダ値ではなく)ヘッダ名をいじれる状況は殆どありえないし、変なヘッダ名をHTTP/2ヘッダにそのまま送り込むようなWebアプリケーション実行環境が存在するか甚だ疑問ではあるが、一応この問題をMozillaに報告したところ、既に別の方が「スペース等がヘッダ名に許されている」というバグを報告していることが分かった。先日リリースされたFirefox 93からはエラー画面を表示するよう変更されている。

なお、Firefoxに関してはこの辺のチェックが緩かったようで、v92にてHTTP/3のヘッダインジェクションも修正されている。

HTTP/2の仕様

HTTP/2の仕様(RFC 7540)はどうなっているかと言うと、10.3等に規定がある。

  • ヘッダ値にCR, LF, NULが含まれる場合はmalformedと扱う。
  • ヘッダ名についてもhttp/1.1的に不正なものはmalformedと扱う。

当然、http/1.1の仕様はヘッダ名に改行文字を許可していないので、ヘッダ名/値ともに改行が含まれるならば何らかのエラーとしなければならない。

ちなみに「malformedと扱う」のは個々のヘッダではなくて要求/応答メッセージ全体であり、クライアントであればエラー画面を表示し、サーバであればステータス400等を応答するべきなのだろう。

HTTP/2固有の応答ヘッダ

最後に、HTTP/2に特有のヘッダで、ヘッダインジェクションで使えるものがあるか?という点について。

例えば、前記事のAlt-Svcは、ChromeではHTTP/2でしか動かないようなので、ChromeにとってはHTTP/2に特有のヘッダと言える。他に何かHTTP/2特有のヘッダがあったかなと思ってざっと見直してみた。結論を言うと「今のところ使えそうなものは無さそう」なのだが、一応調べたことを書いておく。

HTTP/2のヘッダとしては以下の2種類がある。

  • 通常のヘッダ
  • 疑似ヘッダ

まず通常のヘッダをざっと調べたがめぼしい情報は見つからなかった。

次の疑似ヘッダだが、これは先頭に:が付いたHTTP/2固有のヘッダだ。応答で使える疑似ヘッダは:statusの1つのみだが、これをうまく使えば(CGIstatusヘッダと同じように)応答のステータスコードが変えられるかもと思い、NginxのCGIで試してみた。

ヘッダインジェクションがあるCGIに、以下のパラメータを与える。

redirect=ZZZZZ%0D%0A:status: 200

CGIプログラムはパラメータの値をLocationヘッダに出力する。

サーバが出力する応答ヘッダの全体は以下のようになる(HTTP/1的な形式で表現)。

:status: 302
server: nginx/1.20.1
date: Tue, 21 Sep 2021 14:59:34 GMT
content-type: text/html; charset=iso-8859-1
location: ZZZZZ
:status: 200  ← インジェクトされた部分

CGIなので単一のHTTP/2ヘッダの中に改行が入るのではなく、インジェクトした:statusは独立したヘッダとなる。

意図どおり応答には2つの:statusが含まれるが、全てのメジャーブラウザがこの応答をエラーとした。これはおそらく、HTTP/2仕様(8.1.2.1)の「疑似ヘッダは通常のヘッダより前に位置しなければならない」というチェックをブラウザが行っているためだろう*1

ちなみにBurp経由にすると、下のようにBurpのEvent logに「Normalized malformed HTTP/2 response」というメッセージが出る。

しかし、Burpが2つ目の:statusに一本化する形で正常なヘッダに書き換えるため、ブラウザに届くのは正常なステータス200の応答になる。要はBurpがある時だけは:statusヘッダのインジェクションは成功する。

調べていないが、一般のリバースプロキシ等で同じような挙動をするものも、ひょっとしたらあるかもしれない(ただし、使える可能性で言えば、:無しのstatusヘッダの方が大きいだろう)。

診断の観点では、HTTP/2においては「Burpを挟んでいる時だけ動作するHTTPヘッダインジェクションのPoC」というのが存在するので気に留めておく必要があるかもしれない(BurpはHTTP/1でも応答をnormalizeするので、HTTP/2でも/1でも発生しうることではある)。

*1:本来は、ヘッダを出力するWebサーバ側でも、何らかのチェックが必要なのだろう。