読者です 読者をやめる 読者になる 読者になる

てきとうなメモ

本の感想とか技術メモとか

Kindle Unlimitedで良さげな技術書

読んでないもの多いけど漁ってみた。独断と偏見で。

(2016/08/12 追記) 今確認するとほとんどUnlimitedではなくなっている(´・ω・`)

絵本シリーズ

アルゴリズムの絵本 プログラミングが好きになる9つの扉

アルゴリズムの絵本 プログラミングが好きになる9つの扉

パソコンの仕組みの絵本

パソコンの仕組みの絵本

Rubyの絵本 スクリプト言語を楽しく学ぶ9つの扉

Rubyの絵本 スクリプト言語を楽しく学ぶ9つの扉

HTML5の絵本

HTML5の絵本

Visual Basicの絵本

Visual Basicの絵本

Objective-Cの絵本

Objective-Cの絵本

VBAの絵本 VBAが楽しくわかる9つの扉

VBAの絵本 VBAが楽しくわかる9つの扉

Androidの絵本 スマートフォンアプリ開発を始める9つの扉

Androidの絵本 スマートフォンアプリ開発を始める9つの扉

UNIXの絵本 UNIXが楽しくわかる9つの扉

UNIXの絵本 UNIXが楽しくわかる9つの扉

ASP.NET の絵本

ASP.NET の絵本

Cの絵本 C言語が好きになる9つの扉

Cの絵本 C言語が好きになる9つの扉

TCP/IPの絵本 ネットワークが面白くなる9つの扉

TCP/IPの絵本 ネットワークが面白くなる9つの扉

Perlの絵本 Perlが好きになる9つの扉

Perlの絵本 Perlが好きになる9つの扉

C++の絵本

C++の絵本

OSの仕組みの絵本

OSの仕組みの絵本

SQLの絵本 データベースがみるみるわかる9つの扉

SQLの絵本 データベースがみるみるわかる9つの扉

アルゴリズムの絵本 プログラミングが好きになる9つの扉

アルゴリズムの絵本 プログラミングが好きになる9つの扉

C#の絵本

C#の絵本

インターネット技術の絵本

インターネット技術の絵本

PHPの絵本

PHPの絵本

Javaの絵本 増補改訂版

Javaの絵本 増補改訂版

JavaScriptの絵本 ホームページ作りが楽しくなる9つの扉

JavaScriptの絵本 ホームページ作りが楽しくなる9つの扉

教科書シリーズ

よくわかるJavaScriptの教科書 教科書シリーズ

よくわかるJavaScriptの教科書 教科書シリーズ

プログラミング入門

Cプログラミング入門以前

Cプログラミング入門以前

コンピュータープログラミング入門以前

コンピュータープログラミング入門以前

Go言語

スターティングGo言語

スターティングGo言語

Python

基礎Python 基礎シリーズ

基礎Python 基礎シリーズ

Rails

実践Ruby on Rails 4 機能拡張編

実践Ruby on Rails 4 機能拡張編

OpenCV

OpenCV 3 プログラミングブック

OpenCV 3 プログラミングブック

GPUプログラミング

CUDA C プロフェッショナル プログラミング impress top gear

CUDA C プロフェッショナル プログラミング impress top gear

フロントエンド

フロントエンドエンジニアのための現在とこれからの必須知識

フロントエンドエンジニアのための現在とこれからの必須知識

HTML&CSS 標準デザイン講座【HTML5&CSS3対応】

HTML&CSS 標準デザイン講座【HTML5&CSS3対応】

ゲームで学ぶJavaScript入門 HTML5&CSSも身につく!

ゲームで学ぶJavaScript入門 HTML5&CSSも身につく!

ゲーム開発

Kinectv2楽しいプログラミング入門 (NextPublishing)

Kinectv2楽しいプログラミング入門 (NextPublishing)

uGUIではじめるUnity UIデザインの教科書

uGUIではじめるUnity UIデザインの教科書

Raspberry Pi

Raspberry Piでデジカメを作ろう!

Raspberry Piでデジカメを作ろう!

ショートコーディング

機械語

31バイトでつくるアセンブラプログラミング ?アセンブラ短歌の世界?

31バイトでつくるアセンブラプログラミング ?アセンブラ短歌の世界?

0と1のコンピュータ世界 バイナリで遊ぼう!

0と1のコンピュータ世界 バイナリで遊ぼう!

Git

Web制作者のためのGit入門

Web制作者のためのGit入門

Linux運用

CentOS 7実践ガイド

CentOS 7実践ガイド

インフラ/Docker

DevOpsを支えるHashiCorpツール大全 Think IT Books

DevOpsを支えるHashiCorpツール大全 Think IT Books

Dockerコンテナ実践検証 (Think IT Books)

Dockerコンテナ実践検証 (Think IT Books)

Docker実践ガイド

Docker実践ガイド

データベース

SQL ゼロからはじめるデータベース操作

SQL ゼロからはじめるデータベース操作

セキュリティ

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

食べる!SSL! ―HTTPS環境構築から始めるSSL入門

食べる!SSL! ―HTTPS環境構築から始めるSSL入門

AI

マッチ箱の脳(AI)―使える人工知能のお話

マッチ箱の脳(AI)―使える人工知能のお話

位置情報技術

位置情報ビッグデータ (NextPublishing)

位置情報ビッグデータ (NextPublishing)

OS

Excel

10倍ラクして成果を上げる 完全自動のExcel術

10倍ラクして成果を上げる 完全自動のExcel術

「あるある」で学ぶ 忙しい人のためのExcel仕事術 (できるビジネスシリーズ)

「あるある」で学ぶ 忙しい人のためのExcel仕事術 (できるビジネスシリーズ)

わずか5分で成果を上げる 実務直結のExcel術

わずか5分で成果を上げる 実務直結のExcel術

その他

Think Simple ―アップルを生みだす熱狂的哲学

Think Simple ―アップルを生みだす熱狂的哲学

Kindle Unlimitedで良さげなコミック

探すとそこそこ見つかった。
完結か連載中かは正確じゃないかも。

完結したもの

全14巻全4巻
琴浦さん(1巻) (マイクロマガジン・コミックス)

琴浦さん(1巻) (マイクロマガジン・コミックス)

全7巻
神さまのつくりかた。 1巻

神さまのつくりかた。 1巻

全14巻全7巻全10巻

全8巻全4巻全2巻
不死身のフジナミ 1

不死身のフジナミ 1

全6巻+特別編全10巻

ブラック・エンジェルズ1

ブラック・エンジェルズ1

全20巻
マーダーライセンス牙&ブラックエンジェルズ 1

マーダーライセンス牙&ブラックエンジェルズ 1

全13巻全3巻全2巻

全5巻
木造迷宮 (RYU COMICS)

木造迷宮 (RYU COMICS)

全12巻全12巻+1

全7巻全3巻全5巻
第七女子会彷徨(1) (RYU COMICS)

第七女子会彷徨(1) (RYU COMICS)

全10巻中9巻

ウツボラ(1)

ウツボラ(1)

全2巻
限界集落(ギリギリ)温泉第一巻

限界集落(ギリギリ)温泉第一巻

全4巻
ナナのリテラシー1 (Kindle Single)

ナナのリテラシー1 (Kindle Single)

全3巻全3巻
青い花(1)

青い花(1)

全8巻

奇子 1

奇子 1

全3巻全5巻

雷火 (1)

雷火 (1)

全15巻

たぶん惑星: 1 (REXコミックス)

たぶん惑星: 1 (REXコミックス)

全2巻

地球へ… (1)

地球へ… (1)

全3巻

賭博黙示録 カイジ 1

賭博黙示録 カイジ 1

全13巻

アシュラ(1)

アシュラ(1)

全3巻

連載中最新刊以外


8巻中7巻まで
かんなぎ: 1 (REXコミックス)

かんなぎ: 1 (REXコミックス)

11巻中10巻まで10巻中9巻まで
中卒労働者から始める高校生活 1

中卒労働者から始める高校生活 1

6巻中5巻まで25巻中23巻まで
KEYMAN(1) (RYU COMICS)

KEYMAN(1) (RYU COMICS)

11巻中11巻
ねこむすめ道草日記(1) (RYU COMICS)

ねこむすめ道草日記(1) (RYU COMICS)

14巻中14巻
ピコピコ少年

ピコピコ少年

superまで
モンスター娘のいる日常(1) (RYU COMICS)

モンスター娘のいる日常(1) (RYU COMICS)

10巻中9巻5巻中4巻
ゆゆ式 1巻

ゆゆ式 1巻

8巻中7巻
アリスと蔵六(1) (RYU COMICS)

アリスと蔵六(1) (RYU COMICS)

6巻中6巻2巻中2巻4巻中4巻
ゆるゆり: 14 (百合姫コミックス)

ゆるゆり: 14 (百合姫コミックス)

14巻中14巻

CVE-2016-6210の修正

OpenSSH 7.3がリリースされて、CVE-2016-6210が修正されたそうだ

sshd(8): Mitigate timing differences in password authentication

that could be used to discern valid from invalid account names
when long passwords were sent and particular password hashing
algorithms are in use on the server. CVE-2016-6210, reported by
EddieEzra.Harari at verint.com

http://www.openssh.com/txt/release-7.3

どう修正されたのかなと。gitリポジトリをcloneしてきてコミットログをみてみた。

$ git show 9286875a
commit 9286875a73b2de7736b5e50692739d314cd8d9dc
Author: Darren Tucker <dtucker@zip.com.au>
Date:   Fri Jul 15 13:32:45 2016 +1000

    Determine appropriate salt for invalid users.
    
    When sshd is processing a non-PAM login for a non-existent user it uses
    the string from the fakepw structure as the salt for crypt(3)ing the
    password supplied by the client.  That string has a Blowfish prefix, so on
    systems that don't understand that crypt will fail fast due to an invalid
    salt, and even on those that do it may have significantly different timing
    from the hash methods used for real accounts (eg sha512).  This allows
    user enumeration by, eg, sending large password strings.  This was noted
    by EddieEzra.Harari at verint.com (CVE-2016-6210).
    
    To mitigate, use the same hash algorithm that root uses for hashing
    passwords for users that do not exist on the system.  ok djm@

システム上のrootが使っているハッシュアルゴリズムと同じものを利用するようにしたと。なるほど。

openbsd-compat/xcrypt.cのpick_salt関数で/etc/shadowのrootのエントリから$id$salt$の部分を取得している。

/*
 * Pick an appropriate password encryption type and salt for the running
 * system.
 */
static const char *
pick_salt(void)
{
       struct passwd *pw;
       char *passwd, *p;
       size_t typelen;
       static char salt[32];

       if (salt[0] != '\0')
               return salt;
       strlcpy(salt, "xx", sizeof(salt));
       if ((pw = getpwuid(0)) == NULL)
               return salt;
       passwd = shadow_pw(pw);
       if (passwd[0] != '$' || (p = strrchr(passwd + 1, '$')) == NULL)
               return salt;  /* no $, DES */
       typelen = p - passwd + 1;
       strlcpy(salt, passwd, MIN(typelen, sizeof(salt)));
       explicit_bzero(passwd, strlen(passwd));
       return salt;
}

ただ、最新版のopenbsd-compat/xcrypt.cを見るとrootがパスワード持っているとは限らないので、初めて見つけたDES以外のエントリの$id$salt$を返すようになっていた。

/*
 * Pick an appropriate password encryption type and salt for the running
 * system by searching through accounts until we find one that has a valid
 * salt.  Usually this will be root unless the root account is locked out.
 * If we don't find one we return a traditional DES-based salt.
 */
static const char *
pick_salt(void)
{
        struct passwd *pw;
        char *passwd, *p; 
        size_t typelen;
        static char salt[32];

        if (salt[0] != '\0')
                return salt;
        strlcpy(salt, "xx", sizeof(salt));
        setpwent();
        while ((pw = getpwent()) != NULL) {
                passwd = shadow_pw(pw);
                if (passwd[0] == '$' && (p = strrchr(passwd+1, '$')) != NULL) {
                        typelen = p - passwd + 1;
                        strlcpy(salt, passwd, MIN(typelen, sizeof(salt)));
                        explicit_bzero(passwd, strlen(passwd));
                        goto out;
                }   
        }   
 out:
        endpwent();
        return salt;
}

紙のコミックとKindleコミックで発売日が同じもの 2016年09月分

ハッピィミリィ 1 (ジャンプコミックスDIGITAL)

ハッピィミリィ 1 (ジャンプコミックスDIGITAL)

opensshでユーザの存在を確認できる脆弱性(CVE-2016-6210)

Full Disclosure: opensshd - user enumeration

When SSHD tries to authenticate a non-existing user, it will pick up a fake password structure hardcoded in the SSHD
source code. On this hard coded password structure the password hash is based on BLOWFISH ($2) algorithm.
If real users passwords are hashed using SHA256/SHA512, then sending large passwords (10KB) will result in shorter
response time from the server for non-existing users.

opensshは存在しないユーザでログインしようとするとハードコードされた偽のパスワードのハッシュ値に対して認証しようとする。で、ハッシュ値のパスワードを存在するユーザならば通常SHA256($5)やSHA512($6)でハッシュを計算しているはずが、偽のユーザの場合はハードコードされているハッシュ値がBlowfish($2)であるため、存在するユーザと比べて計算が早く終了するので、ユーザの存在をチェックすることができるという話のようだ。

auth.cのfakepwで確かにハードコードしているな。

struct passwd *
fakepw(void)
{
        static struct passwd fake;

        memset(&fake, 0, sizeof(fake));
        fake.pw_name = "NOUSER";
        fake.pw_passwd =
            "$2a$06$r3.juUaHZDlIbQaO2dS9FuYxL1W9M81R1Tc92PoSNmzvpEqLkLGrK";
#ifdef HAVE_STRUCT_PASSWD_PW_GECOS
        fake.pw_gecos = "NOUSER";
#endif
        fake.pw_uid = privsep_pw == NULL ? (uid_t)-1 : privsep_pw->pw_uid;
        fake.pw_gid = privsep_pw == NULL ? (gid_t)-1 : privsep_pw->pw_gid;
#ifdef HAVE_STRUCT_PASSWD_PW_CLASS
        fake.pw_class = ""; 
#endif
        fake.pw_dir = "/nonexist";
        fake.pw_shell = "/nonexist";

        return (&fake);

Apache Commons FileUploadの脆弱性(CVE-2016-3092)

JVNDB-2016-000121 - Apache Commons FileUpload におけるサービス運用妨害 (DoS) の脆弱性

Apache Commons FileUploadにDoS攻撃脆弱性なのだが、攻撃方法の詳細は説明されていない。調べたところ、推測なのだが、2年前のDoS攻撃脆弱性(CVE-2014-0050)と関係するものではないかと思う。

2年前の脆弱性はこちら。

JVN#14876762: Apache Commons FileUpload におけるサービス運用妨害 (DoS) の脆弱性

この脆弱性はboundaryに長い文字列を指定してマルチパートのPOSTリクエストを送ると発生し、無限ループに陥ることがあるというものである。

この脆弱性に対するPoCはネットを探せば見つかる。どうも以下の2つの条件を満たすと攻撃できるようだ

  • boundaryを4092バイト以上にする
  • bodyに4097バイト以上のboundaryを含まないデータを入れる

適当にサーブレットを作ってPoCを実行すると無限ループに陥る。サーブレットを停止してスタックトレースを見ると以下でループしているようだ。
commons uploadのバージョンは1.3を用いた

MultipartStream$ItemInputStream.makeAvailable() line: 1016    
MultipartStream$ItemInputStream.read(byte[], int, int) line: 901    
MultipartStream$ItemInputStream(InputStream).read(byte[]) line: 101    
Streams.copy(InputStream, OutputStream, boolean, byte[]) line: 101    
Streams.copy(InputStream, OutputStream, boolean) line: 70    
MultipartStream.readBodyData(OutputStream) line: 589    
MultipartStream.discardBodyData() line: 613    
MultipartStream.skipPreamble() line: 630    
FileUploadBase$FileItemIteratorImpl.findNextItem() line: 1018   
FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase, RequestContext) line: 998    
DiskFileUpload(FileUploadBase).getItemIterator(RequestContext) line: 310    
DiskFileUpload(FileUploadBase).parseRequest(RequestContext) line: 334   
DiskFileUpload(FileUploadBase).parseRequest(HttpServletRequest) line: 288 

ループしているのはMultipartStream$ItemInputStream.makeAvailableだが、このメソッドを説明するためには各クラスの説明を先にした方が良い。
関係する主要なクラスはMultipartStreamとItemInputStreamである。

MultipartStreamはマルチパートのPOSTリクエストをparseするクラスであり、子クラスItemInputStreamはboundaryで分割された現在のitemを読みだすためのクラスである。

この2つのクラスは効率化のために内部のバッファを利用している。このバッファの処理が脆弱性の原因である。

まず、MultipartStreamはバッファ関係のフィールドとして以下のものを持っている。

byte[] buffer バッファを入れるバイト配列
int bufSize bufferのサイズ
int head buffer上の実際のバッファの開始インデックス
int tail buffer上の実際のバッファの終了インデックス
byte[] boundary boundary+prefixを入れておくバイト配列
int boundaryLength boundary.length
int keepRegion boundaryをバッファから探すために必要なサイズ

さらに、ItemInputStream側にもバッファ関係のフィールドとして以下のものがある。

int pos boundaryを検索した時の、boundaryのMultipartStream.buffer上の開始インデックス
int pad boundaryを検索するために保持すべき実際のバイト数。MultipartStream.keepRegionと似たようなもの

headとtailはbuffer内の有効な部分を指している。1バイト読みだすとheadをインクリメントする。head=tailになるとInputStreamから実際に読み出してbufferに入れている。

boundaryはリクエストで指定されたboundaryを入れるバイト列だが、それだけでなくprefixに[CR,LF,-,-]がくっついている。これは実際の区切り文字にはboundaryに--がくっつくためであり、さらに行頭なのでCR,LFがprefixとしてくっついている。この辺りはコンストラクタのコードを見ることで確認できる。

MultipartStream(InputStream input,
      byte[] boundary,
      int bufSize,
      ProgressNotifier pNotifier) {
  ...
  // BOUNDARY_PREFIXは[CR, LF, -, -]
  this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
  this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
  this.keepRegion = this.boundary.length;
  System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
          BOUNDARY_PREFIX.length);
  System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
          boundary.length);
  ...
}

MultipartStream内では、buffer内からboundaryを検索する際にboundaryの途中までマッチしたという情報を残していない。そのままだと読み込んだ単位の境にboundaryがある場合は検知できない。そのため、バッファを全て読み込んでしまい入力ストリームからバッファに読み込む時に、boundaryのサイズだけは常にバッファに残しておき、残りの部分に入力ストリームからデータを書き込む必要性がある。このboundaryの検索のために保持すべきサイズがkeepRegionであり、padは

pad = min(tail-head, keepRegion)

である。

ItemInputStream.makeAvailableの話に戻る。makeAvailableは読み込み可能なデータがバッファ内にない場合にデータを入力ストリームから読み込みバッファに入れて、読み込み可能なバイト数を返すメソッドである。

private int makeAvailable() throws IOException {
    // boundaryが見つかったので終了
    if (pos != -1) {
        return 0;
    }   

    // head〜tailのうち後ろ側のpad分をbufferの最初にもってくる
    // Move the data to the beginning of the buffer.
    total += tail - head - pad;
    System.arraycopy(buffer, tail - pad, buffer, 0, pad);

    // 有効なのは0〜padになる
    // Refill buffer with new data.
    head = 0;
    tail = pad;

    for (;;) {
        // bufferの有効ではない部分に入力から読み込む
        int bytesRead = input.read(buffer, tail, bufSize - tail);
        if (bytesRead == -1) {
            // The last pad amount is left in the buffer.
            // Boundary can't be in there so signal an error
            // condition.
            final String msg = "Stream ended unexpectedly";
            throw new MalformedStreamException(msg);
        }   
        if (notifier != null) {
            notifier.noteBytesRead(bytesRead);
        }   

        // tailを読んだ分だけ拡張する
        tail += bytesRead;

        // buffer内からboundaryを検索し、その位置をposに入れる
        // boundaryが見つからなければ、pos=-1になる
        findSeparator();
        // bufferから読み込み可能なデータを探して、読み込み可能なバイト数を返す
        int av = available();

        // 読み込み可能なデータがあるか、boundaryが見つかった場合はバイト数を返す
        if (av > 0 || pos != -1) {
            return av; 
        }   
    }   
}

available()は現在の読み込み可能なバイト数を返すが、ちょっと特殊で以下のようになっている。

public int available() throws IOException {
  if (pos == -1) {
    return tail - head - pad;
  }
  return pos - head;
}

buffer内にboundaryが見つかった場合はboundaryの開始インデックスまでのサイズを返すが、見つからなければ今有効なサイズ(tail-head)からboundaryに必要なサイズを除いたサイズを返している。boundaryのサイズ分はすぐには使えないと考えているようだ。

で、ループしているのはmakeAvailableのfor文の部分である。

このforループが以下のような条件で攻撃された時の動作を考えてみる。

  • boundaryを4092文字の文字列とし、
  • 入力ストリームがboundaryのない4097文字のバイト列とする。

初期状態は以下のようになっている

  • head=tail=0
  • bufferは用意されているが、0 fillされている
  • bufSize=4096(デフォルト)
  • boundaryにはCR,LF,-,-,boundaryの値が入っている。よって4096文字
  • keepRegionはboundary.lengthなので4096

すると、以下のように処理が進む

  1. 初期状態ではhead=tail=0となっている。
  2. input.readでバッファを読んでいき、tailも読んだサイズに拡張される。
  3. 初回でバッファのサイズ文読み込まれるとは限らないが、バッファサイズまで読み込まれない場合でも、boundaryが見つからずpos=-1になり、available()もtail-head-pad=tail-head-min(tail-head, 4096)=tail-head-(tail-head)=0を返すので、次のループでバッファ全体に読み込まれる
  4. バッファ全体を読み込んだ時には以下のようになり、ループ条件を抜けられない
    • head = 0, tail = 4096
    • pad = min(tail-head, 4096) = 4096
    • pos = -1 (boundaryは発見できない)
    • available() = tail-head-pad = 0
  5. その後も上記の状態のままinput.readしても0バイト読み込むだけなので終了しない

となって無限ループする。

commons fileuploadのv1.3.1ではこの問題が修正されており、以下のコードが追加されている。

@@ -331,9 +326,14 @@
 
         // We prepend CR/LF to the boundary to chop trailing CR/LF from
         // body-data tokens.
-        this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
         this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
+        if (bufSize < this.boundaryLength + 1) {
+            throw new IllegalArgumentException(
+                    "The buffer size specified for the MultipartStream is too small");
+        }
+        this.boundary = new byte[this.boundaryLength];
         this.keepRegion = this.boundary.length;
+
         System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
                 BOUNDARY_PREFIX.length);
         System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,

boundaryLengthはリクエスト指定したboundaryのサイズ+4なので4092バイト以上のboundaryが指定された場合はエラーになるようになった。

さて、大元のCVE-2016-3092の話に戻る。boundaryが4092バイト以上の場合は良いが4091バイトだったらどうか。
バッファいっぱいまで読み込んだ状態では

  • head=0,tail=4096
  • keepRegion=4091+4=4095
  • pad=min(tail-head, keepRegion)=min(4096,4095)=1

となり、availableは1を返す。ではmakeAvailableを読んでいる方は以下のコードになる

public int read(byte[] b, int off, int len) throws IOException {
  if (closed) {
    throw new FileItemStream.ItemSkippedException();
  }
  if (len == 0) {
    return 0;
  }
  int res = available();
  if (res == 0) {
    // resに1が入る
    res = makeAvailable();
    if (res == 0) {
      return -1;
    }
  }
  res = Math.min(res, len);
  // 1バイトしかコピーされない
  System.arraycopy(buffer, head, b, off, res);
  head += res;
  total += res;
  return res;
}

1バイトに対するSystem.arraycopyが発生する。リクエストボディが小さければ大したことないが1MB程度のリクエストを送るとこれを1バイトずつ処理して100万回近くループが発生してしまう。おそらくこれがCVE-2016-3092の脆弱性ではないかと思う。

This caused the file
upload process to take several orders of magnitude longer than if the
boundary length was the typical tens of bytes.

http://mail-archives.apache.org/mod_mbox/www-announce/201606.mbox/%3C6223ece6-2b41-ef4f-22f9-d3481e492832@apache.org%3E

と無限ループになるわけではないし、今回の修正のdiffをみるとコンストラクタで最低でもboundaryLengthの2倍をとるようになっているという点とも合致する。

@@ -325,12 +325,6 @@
         if (boundary == null) {
             throw new IllegalArgumentException("boundary may not be null");
         }
-
-        this.input = input;
-        this.bufSize = bufSize;
-        this.buffer = new byte[bufSize];
-        this.notifier = pNotifier;
-
         // We prepend CR/LF to the boundary to chop trailing CR/LF from
         // body-data tokens.
         this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
@@ -338,6 +332,12 @@
             throw new IllegalArgumentException(
                     "The buffer size specified for the MultipartStream is too small");
         }
+
+        this.input = input;
+        this.bufSize = Math.max(bufSize, boundaryLength * 2);
+        this.buffer = new byte[this.bufSize];
+        this.notifier = pNotifier;
+
         this.boundary = new byte[this.boundaryLength];
         this.keepRegion = this.boundary.length;

こうすることで、バッファの半分程度はavailable()で返るようになり、デフォルトのバッファのサイズが4KBなので最低でも2KB程度ずつは処理されることになる。

プリンシプル オブ プログラミング

良いプログラミングに必要な原則、手法をまとめた本。著者は具体例やコードを用いないというポリシーで書いているのだが、やはり具体例がないと頭に入ってこない感じがしてよくないな。