Apache HTTPD 2.2.20 における変更点 (CVE-2011-3192 Range header DoS 対策)

既にヘッダ削除設定で対応を終えた人も多いと思いますが、新しいバージョンが出た様なので。
まだ未対策の人は、どちらの対応がリスクが低いか判断して対策されると良いかと思います。

新バージョンのアナウンスは以下です。


他に変わってる箇所もありますが、今回の脆弱性(CVE-2011-3192)対応で修正された
箇所で、外部からアクセスした際にも変更がみられる点がメインになります。
当然プロセスが肥大化する部分は直ってますが、内部動作なので詳細は省略します。
バージョンは2.2.19と2.2.20の比較になります。


Rangeリクエスト関連の処理で変更があった、無かった箇所は以下の通りです。

  • 動作が変わった点
    • 32区間を超える応答にてContent-Lengthヘッダが使用されなくなった
    • 合計値がコンテンツサイズを超える複数区間要求を処理しなくなった
    • 負値のみを指定した場合の動作が変更された(多分リグレッション)
    • Rangeリクエストで全区間要求した場合にレスポンスコードが異なる(2011/09/01追記)
  • 動作が変わらなかった点
    • Rangeヘッダでの区間数制限(無しのまま)
      • 以前同様リクエストヘッダの最大長依存
      • ヘッダ削除設定で対応した場合はこの部分に制限が掛かっている状態
    • Rangeヘッダで重複する区間指定をした場合のレスポンス(マージしない、要求されたまま返す)
      • trunkでは途中まで重複区間をマージする動作に変更されていた。
      • が、2.2.xでは以前と変わらない動作になった。現在のtrunk版はどうなっているか未確認


まず、一つめの動作が変わった点です。
32区間を超える要求した場合以下の通りレスポンスが変わります。
chunkedはHTTP/1.1の機能となるので、HTTP/1.0の場合は使用できません。
そのためHTTP/1.0かつ指定区間以上要求した場合はKeep-Aliveが使用不可になります。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=100-200,200-300,...
## 2.2.19 と 2.2.20の32区間以下 の場合のレスポンス
HTTP/1.1 206 Partial Content
Content-Length: xxxxx
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
## 2.2.20の32区間を超えるレスポンス
HTTP/1.1 206 Partial Content
Transfer-Encoding: chunked
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...

ソースコードの該当箇所はこの辺です。
何故32なのかまでは追っていませんがフィルタやブリッジの制約なのかもしれません。
# 0x1F=31だけどiは0から始まるので32区間まではOK
更にこの部分の処理で、プロセス肥大化要因となっていたapr_bucketのコピー処理が
置き換わっているので大量の区間を処理する場合においてもプロセス肥大化しなくなっています。

@@httpd-2.2.20/modules/http/byterange_filter.c
        APR_BRIGADE_CONCAT(bsend, tmpbb);
        if (i && !(i & 0x1F)) {
            /*
             * Every now and then, pass what we have down the filter chain.
             * In this case, the content-length filter cannot calculate and
             * set the content length and we must remove any Content-Length
             * header already present.
             */
            apr_table_unset(r->headers_out, "Content-Length");
            if ((rv = ap_pass_brigade(f->next, bsend)) != APR_SUCCESS)
                return rv;
            apr_brigade_cleanup(bsend);
        }

動作は変わってはいますが制限が追加された訳ではないので、HTTP/1.1準拠の
クライアント実装であれば特に問題にはならないと思われます。chunkedの処理で問題が
起きる様なクライアント実装であれば既に別の問題を踏んでいるでしょう。


次に二つめの変更点です。
合計値がコンテンツサイズを超える複数区間要求を処理しなくなりました。
と、書くとなんだか解りにくいですが要は Range: bytes=0-,0-,0-,... 等への負荷対策だと思われます。
例を挙げるとこんな感じです。リクエスト対象のコンテンツは1000bytesだと思ってください。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-,0-,0-
## 2.2.19のレスポンス (元コンテンツ*3 + マルチパートバウンダリ等 のサイズになる)
HTTP/1.1 206 Partial Content
Content-Length: 3500
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
## 2.2.20のレスポンス (無視してRangeヘッダが無かったのと同じ扱いになる)
HTTP/1.1 200 OK
Content-Length: 1000
...

ソースコードの該当箇所はこの辺です。
sum_lengthsは要求した複数区間の合計値、clengthは対象のコンテンツのサイズが入ります。

@@httpd-2.2.20/modules/http/byterange_filter.c
    if (sum_lengths >= clength) {
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "Sum of ranges not smaller than file, ignoring.");
        return 0;
    }

2.2.20においても合計値が超えない場合は2.2.19と同じ動作になります。
こちらも行儀の悪い使い方してない限りはまず問題にならない修正だと思います。


最後に三つめです。
区間指定で負値を指定した場合の動作が変更されました。
ソース見た感じ負値を考慮しようとしてるので、意図した変更では無い感じですね。
これは単一、複数区間のリクエスト問わず両方に影響する変更となります。
以下は1000bytesコンテンツに対してリクエストした場合の動作です。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=-10
## 2.2.19のレスポンス (負の値として扱われて後ろの部分が返ってくる)
HTTP/1.1 206 Partial Content
Content-Length: 10
Content-Range: bytes 990-999/1000
...
## 2.2.20のレスポンス (終点のみ指定された物として扱われる)
HTTP/1.1 206 Partial Content
Content-Length: 11
Content-Range: bytes 0-10/1000
...

異なる解釈になっている事が分かると思います。

ソースコードは以下の箇所になります。

## 2.2.19 始点が無ければ負値として扱っている
@@httpd-2.2.19/modules/http/byterange_filter.c
static int parse_byterange(char *range, apr_off_t clength,
                           apr_off_t *start, apr_off_t *end)
{
    char *dash = strchr(range, '-');
    char *errp;
    apr_off_t number;

    if (!dash) {
        return 0;
    }

    if ((dash == range)) {
        /* In the form "-5" */
        if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
            return 0;
        }
        *start = clength - number;
        *end = clength - 1;
    }
    else {
...
## 2.2.20 同じ様にしたつもりが間違えた?(dash == range ではなく dash == cur)
@@httpd-2.2.20/modules/http/byterange_filter.c
static int ap_set_byterange(request_rec *r, apr_off_t clength,
                            apr_array_header_t **indexes)
...
    *indexes = apr_array_make(r->pool, ranges, sizeof(indexes_t));
    while ((cur = ap_getword(r->pool, &range, ','))) {
        char *dash;
        char *errp;
        apr_off_t number, start, end;

        if (!(dash = strchr(cur, '-'))) {
            break;
        }

        if (dash == range) {
            /* In the form "-5" */
            if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
                break;
            }
            start = clength - number;
            end = clength - 1;
        }
        else {
...

と、結果として解釈が変わっているので返ってくるバイト数が変わってしまっています。
問題になる可能性が一番高いのはこの動作変更ですね。(というよりバグ)
以下の様に修正したら以前同様の動作になりましたが、他にも問題があるかも知れないので
公式修正を待つのが無難かと思います。

@@httpd-2.2.20/modules/http/byterange_filter.c
        if (!(dash = strchr(cur, '-'))) {
            break;
        }

-       if (dash == range) {
+       if (dash == cur) {
            /* In the form "-5" */
            if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
                break;
            }

この値の扱いについては2.2.19の動作で正しい様です。
わざわざRFC違反するように直したりはしないと思うのでリグレッションでしょうね。
以下は RFC 2616 の 14.35.1 Byte Ranges から抜粋。

   Examples of byte-ranges-specifier values (assuming an entity-body of
   length 10000):
      - The first 500 bytes (byte offsets 0-499, inclusive):  bytes=0-
        499
      - The second 500 bytes (byte offsets 500-999, inclusive):
        bytes=500-999
      - The final 500 bytes (byte offsets 9500-9999, inclusive):
        bytes=-500
      - Or bytes=9500-
      - The first and last bytes only (bytes 0 and 9999):  bytes=0-0,-1
      - Several legal but not canonical specifications of the second 500
        bytes (byte offsets 500-999, inclusive):
         bytes=500-600,601-999
         bytes=500-700,601-999


上記の通り、新しいバージョンにて変更された動作はいくつかあります。
変わった使い方をしていない限りは気付かない変更だとは思いますが、
全く問題なしというわけでもなく、既に踏んでしまった人も居るようです。


報告されているのはDebianが先にバックポートしたバージョンなので、2.2.20と
全く同じではありませんが、内容的には今回の修正に関連する所だと思われます。
(現時点ではリクエストヘッダ等が出てないので詳細は分かりませんが)


======
他にも変わってる点があったので追記(2011/09/01)
まだありそうな感じもしますが。

Rangeリクエストで全区間要求した場合にレスポンスコードが異なります。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-
## 2.2.19 のレスポンス 206で全区間が返る
HTTP/1.1 206 Partial Content
Content-Length: 1000
Content-Range: bytes 0-999/1000
...
## 2.2.20 のレスポンス 200で全区間が返る
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 1000
...

ソースコード的には二つめの変更点の箇所ですね。
trunkのコードでは判定条件が >= から > に変わっているので
次のリリースでは直るんじゃないかと思います。
Debianで報告されてたバグはこれの様な感じがしますね。


Redhatでもbugzilla見ると指摘はされていますがそのままリリースされてる感じが。(良いのかこれ?)
Debianの方は最初は同じでしたが、報告あった後に修正したバージョンを作ろうとしてるようですね。