nginxのchunked取扱い脆弱性と、Apacheでchunkedさせない方法

かなり旧聞だけど、セキュリティホールmemoさんところで知ったnginxの脆弱性
https://www.st.ryukoku.ac.jp/~kjm/security/memo/2013/05.html#20130513_nginx
nginx 1.3.9から1.4.0までchunkedの扱いに問題があり、stack buffer overflowの脆弱性があったそうだ。1.5.0 or 1.4.1で修正されているらしい。

脆弱性の中身

もう少し詳しくパッチを見てみると、こんな感じになっていた。

--- src/http/ngx_http_parse.c
+++ src/http/ngx_http_parse.c
@@ -2209,6 +2209,10 @@ data:
 
     }
 
+    if (ctx->size < 0 || ctx->length < 0) {
+        goto invalid;
+    }
+
     return rc;
 
 done:

ctxというのはチャンクサイズを格納しているのだが、64bit符号ありから32bit符号無しにキャストする際の値チェックがされておらず、負数を突っ込むことでバッファオーバーフローが起きていたようだ。まぁ、よくあること。

chunkedとは何ぞや

と、これだけで終わってもなんなので、HTTPのchunked形式について少し語る。以下、ここまでの流れを完全にぶった切ってApacheの話しかしないけど、気にしないでください。

chunkedレスポンスのサンプル

chunkedはHTTP 1.1で登場した形式である。See: RFC2068

サンプルをまず見てみると、あるHTTPリクエストに対して、以下のようなレスポンスがchunked形式である。

HTTP/1.1 200 OK
Date: Sat, 27 Jul 2013 22:01:45 GMT
Server: Apache/2.2.15 (CentOS)
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

100
<html>
<head>
<title>title</title>
</head>
<body>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
</body>
</html>

0

普通のHTMLを返しているだけ……と思いきや、ところどころに変な数字が入っている。実はこれは転送バイト数で、メッセージボディをchunk(固まり)に分け、それぞれのバイト数を頭に付けることで細切れに転送することを可能としているのだ。

だからここではまず100(16進数の0x100なので実際は1*16*16=256バイト)を送り、次に0バイトを送り、それで終了。ということになる。もちろんブラウザはこれを適切に組み立てるので、100とか0とかの数字が表示されることはない。

ApacheCGIとchunked

さて、ApacheCGIを動かすと、なるたけレスポンスはchunkedで返すようになっている。上記のchunkedレスポンスの例も、以下のような簡単なCGIで返したモノだ。

#!/usr/bin/perl

use strict;
use warnings;

my $body = <<"EOM";
<html>
<head>
<title>title</title>
</head>
<body>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
</body>
</html>
EOM

print "Content-Type: text/html\n";
print "\n";

print $body;

普通にブラウザで見るとchunkedで返しているか確認できないので、コマンドプロンプトなどから手でhttp通信すれば確認できる。

$ telnet 192.168.2.5 80
GET /cgi-bin/chunk/chunked.cgi HTTP/1.1
HOST: 192.168.2.5

こう叩けば、上記のレスポンスが確認できる。(HTTP 1.1なので、GETだけでなくHOSTヘッダも必要なことに注意)

chunked未対応とHTTP/1.0とHTTP/1.1

さて、chunkedはHTTP/1.1で定義されたものなので、HTTP/1.0でGETしてきたクライアントに対してchunkedで返すのはNGである。また、クライアントはHTTP/1.1でGETするならば当然chunkedに対応していないといけない。

……しかしこれがなかなか微妙で、世の中には「HTTP/1.0でGETしているのにchunkedで返すサーバ」とか、「HTTP/1.1で取りに来ているくせにchunked対応していないクライアント」がいっぱいある。私の経験で言うと、組み込み系の「なんちゃってHTTP」みたいのに多い。

で、そういう困ったちゃんのためにApacheでchunked返さないようにするにはどうすればいいかというのを少し試行錯誤してみた。

Content-Lengthを付ければよい

さてHTTP/1.1では、Content-Lengthヘッダとchunkedの両方を含んではいけない(MUST NOT)と書かれている。

Messages MUST NOT include both a Content-Length header field and the "chunked" transfer coding. If both are received, the Content-Length MUST be ignored.

というわけで、CGIの中で自前でContent-Lengthを付けてみるとどうなるかと言うと……先ほどのCGIの最後を、こう変えてみる。

print "Content-Type: text/html\n";
print "Content-Length: " . length($body) . "\n";
print "\n";

print $body;

こうして自前でContent-Lengthヘッダを付けてみると、以下のようにHTTP/1.1で取りに来てもchunkedでは送らないようになった。

$ telnet 10.211.55.18 80
GET /cgi-bin/chunk/nochunked.cgi HTTP/1.1
HOST: 10.211.55.18

HTTP/1.1 200 OK
Date: Sat, 27 Jul 2013 22:14:35 GMT
Server: Apache/2.2.15 (CentOS)
Content-Length: 256
Connection: close
Content-Type: text/html; charset=UTF-8

<html>
<head>
<title>title</title>
</head>
<body>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
</body>
</html>

解決……なのか?

ただこの手法は正直、あまり良い解決方法ではないと思う。Apacheの微妙な動作に依っているので……。あくまでワークアラウンドということで、どうしてもお困りの際はお試し下さい。