web-dev-qa-db-ja.com

Java(DoS攻撃を防ぐため)を使用してファイルまたはストリームを読み取る最も堅牢な方法

現在、InputStreamを読み取るための以下のコードがあります。ファイル全体をStringBuilder変数に保存し、その後この文字列を処理しています。

_public static String getContentFromInputStream(InputStream inputStream)
// public static String getContentFromInputStream(InputStream inputStream,
// int maxLineSize, int maxFileSize)
{

    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String lineSeparator = System.getProperty("line.separator");
    String fileLine;

    boolean firstLine = true;
    try {
        // Expect some function which checks for line size limit.
        // eg: reading character by character to an char array and checking for
        // linesize in a loop until line feed is encountered.
        // if max line size limit is passed then throw an exception
        // if a line feed is encountered append the char array to a StringBuilder
        // after appending check the size of the StringBuilder
        // if file size exceeds the max file limit then throw an exception

        fileLine = bufferedReader.readLine();

        while (fileLine != null) {
            if (!firstLine) stringBuilder.append(lineSeparator);
            stringBuilder.append(fileLine);
            fileLine = bufferedReader.readLine();
            firstLine = false;
        }
    } catch (IOException e) {
        //TODO : throw or handle the exception
    }
    //TODO : close the stream

    return stringBuilder.toString();

}
_

コードはセキュリティチームとのレビューに行き、次のコメントが寄せられました。

  1. _BufferedReader.readLine_はDOS(Denial of Service)攻撃の影響を受けやすい(無限長の行、改行/キャリッジリターンを含まない巨大なファイル)

  2. StringBuilder変数のリソースの枯渇(使用可能なメモリより大きいデータを含むファイルの場合)

以下は私が考えることができる解決策です:

  1. readLineメソッドの代替実装(readLine(int limit))を作成し、noをチェックします。読み込まれたバイト数が指定された制限を超えた場合、カスタム例外をスローします。

  2. ファイル全体をロードせずに、ファイルを1行ずつ処理します。 (純粋な非Javaソリューション:))

上記のソリューションを実装する既存のライブラリがあるかどうかを提案してください。また、提案されているものよりも堅牢性が高い、または実装がより便利な代替ソリューションを提案します。パフォーマンスも重要な要件ですが、セキュリティが最初になります。

21
Unni Kris

更新された回答

あらゆる種類のDOS攻撃(行、ファイルのサイズなど)を避けたい。しかし、関数の最後では、ファイル全体を単一のString !!!に変換しようとしています。行を8 KBに制限すると仮定しますが、誰かが2つの8 KB行を含むファイルを送信するとどうなりますか?行読み取り部分は通過しますが、最終的にすべてを単一の文字列に結合すると、文字列は使用可能なすべてのメモリを詰まらせます。

したがって、最終的にはすべてを単一の文字列に変換するので、行サイズを制限することは重要ではなく、安全でもありません。ファイル全体のサイズを制限する必要があります。

第二に、あなたが基本的にやろうとしていることは、チャンクでデータを読み取ろうとしているということです。したがって、BufferedReaderを使用し、1行ずつ読み取っています。しかし、あなたがやろうとしていること、そして最後に本当に欲しいのは、ファイルを少しずつ読み取る何らかの方法です。一度に1行ずつ読み取るのではなく、一度に2 KBずつ読み取ってください。

BufferedReader-名前で-内部にバッファがあります。そのバッファーを構成できます。バッファーサイズが2 KBのBufferedReaderを作成するとします。

BufferedReader reader = new BufferedReader(..., 2048);

InputStreamに渡すBufferedReaderに100 KBのデータがある場合、BufferedReaderは一度に2 KBを自動的に読み取ります。そのため、ストリームを50回、それぞれ2 KB(50x2KB = 100 KB)読み取ります。同様に、10 KBのバッファーサイズでBufferedReaderを作成すると、入力を10回読み取ります(10x10KB = 100 KB)。

BufferedReaderは、既にチャンクごとにファイルを読み取る作業を行っています。そのため、その上に行ごとに余分なレイヤーを追加する必要はありません。最終結果に焦点を合わせます-最後のファイルが大きすぎる(>使用可能なRAM)場合-最後にどのようにStringに変換しますか?

より良い方法の1つは、単にCharSequenceとして渡すことです。それがAndroidが行うことです。 Android API全体で、CharSequenceがどこでも返されることがわかります。 StringBuilderCharSequenceのサブクラスでもあるため、Androidは内部でStringStringBuilder、または入力のサイズ/性質に基づいて最適化された他の文字列クラスを使用します。したがって、すべてを読んだら、StringBuilderに変換するのではなく、Stringオブジェクト自体を直接返すことができます。これは、大きなデータに対して安全です。 StringBuilderも内部のバッファと同じ概念を維持し、1つの長い文字列ではなく、大きな文字列に複数のバッファを内部的に割り当てます。

全体的に:

  • ある時点でコンテンツ全体を処理するため、全体のファイルサイズを制限します。行の制限や分割を忘れる
  • チャンクで読む

Apache Commons IOを使用して、次のようにBoundedInputStreamからStringBuilderにデータを読み取り、行ではなく2 KBブロックで分割します。

// import org.Apache.commons.io.output.StringBuilderWriter;
// import org.Apache.commons.io.input.BoundedInputStream;
// import org.Apache.commons.io.IOUtils;

BoundedInputStream boundedInput = new BoundedInputStream(originalInput, <max-file-size>);
BufferedReader reader = new BufferedReader(new InputStreamReader(boundedInput), 2048);

StringBuilder output = new StringBuilder();
StringBuilderWriter writer = new StringBuilderWriter(output);

IOUtils.copy(reader, writer); // copies data from "reader" => "writer"
return output;

元の回答

Apache Commons IO ライブラリから BoundedInputStream を使用します。あなたの仕事はずっと簡単になります。

次のコードはあなたが望むことをします:

public static String getContentFromInputStream(InputStream inputStream) {
  inputStream = new BoundedInputStream(inputStream, <number-of-bytes>);
  // Rest code are all same

InputStreamBoundedInputStreamで単にラップし、最大サイズを指定するだけです。 BoundedInputStreamは、読み取りをその最大サイズまでに制限します。

または、リーダーを作成するときにこれを行うことができます。

BufferedReader bufferedReader = new BufferedReader(
  new InputStreamReader(
    new BoundedInputStream(inputStream, <no-of-bytes>)
  )
);

基本的にここで行っていることは、行を読み取るときにサイズを制限するのではなく、InputStreamレイヤー自体で読み取りサイズを制限することです。したがって、InputStreamレイヤーでの読み取りを制限するBoundedInputStreamのような再利用可能なコンポーネントになり、必要な場所で使用できます。

編集:脚注を追加

編集2:コメントに基づいて更新された回答を追加

37
Subhas

ファイル処理を行うには、基本的に4つの方法があります。

  1. ストリームベースの処理(_Java.io.InputStream_モデル):オプションで、ストリームの周囲にbufferedReaderを配置し、ストリームから次の利用可能なテキストを反復して読み取ります(利用可能なテキストがない場合は、blockいくつかのテキストが利用可能になるまで)、テキストの各部分を読み取り中に個別に処理します(さまざまなサイズのテキスト部分を分類します)

  2. チャンクベースの非ブロッキング処理(_Java.nio.channels.Channel_モデル):一連の固定サイズのバッファー(処理される「チャンク」を表す)を作成し、各バッファーに読み込みますブロックせずにターン(nio APIは高速O/Sレベルスレッドを使用してネイティブIOに委任)、メイン処理スレッドは、他のバッファーが非同期にロードされ続けるため、各バッファーがいっぱいになると順番に固定サイズチャンクを処理します。

  3. パーツファイル処理(行ごとの処理を含む)((1)または(2)を活用して各「パーツ」を分離または構築できます):ファイル形式を意味的に意味のあるサブに分割します-パーツ(可能であれば!行に分割することも可能!)、ストリームピースまたはチャンクを反復処理し、次のパーツが完全にビルドされるまでメモリ内のコンテンツをビルドし、ビルドされるとすぐに各パーツを処理します。

  4. ファイル処理全体(_Java.nio.file.Files_モデル):1回の操作でファイル全体をメモリに読み込み、内容全体を処理します

どちらを使用すべきですか?
ファイルの内容と必要な処理の種類によって異なります。
リソース使用効率の観点から(最高から最低):1,2,3,4。
処理速度と効率の観点から(最高から最低):2,1,3,4。
プログラミングの容易さの観点から(最高から最低):4,3,1,2。
ただし、処理の種類によっては、最小のテキスト(1、おそらく2)を超えるテキストが必要な場合があり、一部のファイル形式には内部部品がない(3を除外)場合があります。

あなたは4をやっています。3(またはそれ以下)にシフトすることをお勧めします。可能であれば

4未満では、DOSを回避する方法は1つしかありません。メモリに読み込む前にサイズを制限します(または、ファイルシステムにコピーします)。読み込まれたら手遅れです。これが不可能な場合は、3、2、または1を試してください。

ファイルサイズの制限

多くの場合、ファイルはHTMLフォームを介してアップロードされます。

サーブレット_@MultipartConfig_アノテーションとrequest.getPart().getInputStream()を使用してアップロードする場合、ストリームから読み取るデータ量を制御できます。また、request.getPart().getSize()は事前にファイルサイズを返します。十分に小さい場合は、request.getPart().write(path)を実行してファイルをディスクに書き込むことができます。

JSFを使用してアップロードする場合、JSF 2.2(非常に新しい)にはmaxLengthの属性を持つ標準のHTMLコンポーネント_<h:inputFile>_(_javax.faces.component.html.InputFile_)があります。 JSF 2.2より前の実装には、同様のカスタムコンポーネントがあります(たとえば、トマホークにはmaxLength属性を持つ_<t:InputFileUpload>_、PrimeFacesにはsizeLimit属性を持つ_<p:FileUpload>_があります)。

ファイル全体を読み取るための代替

InputStreamStringBuilderなどを使用するコードは、効率的ファイル全体を読み取る方法ですが、必ずしも最も簡単なとは限りません方法(コードの最小行)。

ジュニア/平均的な開発者は、ファイル全体を処理しているときに、効率的なストリームベースの処理をしているという誤解を受ける可能性があるため、適切なコメントを含めてください。

より少ないコードが必要な場合は、次のいずれかを試すことができます。

_ List<String> stringList = Java.nio.file.Files.readAllLines(path, charset);

 or 

 byte[] byteContents =  Java.nio.file.Files.readAllBytes(path);
_

しかし、それらには注意が必要です。さもないと、リソースの使用効率が悪くなる可能性があります。 readAllLinesを使用し、List要素を単一のStringに連結すると、メモリを2倍消費します(List要素+連結String)。同様に、readAllBytesを使用し、続いてStringnew String(byteContents, charset))にエンコードすると、再びメモリを「2倍」使用します。ファイルを十分に小さいサイズに制限しない限り、_List<String>_または_byte[]_に対して直接処理するのが最善です。

14
Glen Best

readLineの代わりに、指定された量の文字を読み取るreadを使用します。

各ループで、読み取られたデータの量を確認します。特定の量、予想される入力の最大値を超えている場合、それを停止してエラーを返し、ログに記録します。

3
Christian

追加の注意事項として、BufferedInputStreamを閉じていないことに気付きました。 BufferedReaderはメモリリークの影響を受けやすいため、finallyブロックを閉じる必要があります。

_...
} catch (IOException e) {
        // throw or handle the exception
    } finally{
       bufferedReader.close();
}
_

new InputStreamReader(inputStream)を明示的に閉じる必要はありません。これは、ラッピングクラスbufferedReaderを閉じるために呼び出すと自動的に閉じるためです。

2
mel3kings

巨大なバイナリファイル(通常は改行文字を含まない)をコピーするときに、同様の問題に直面しました。 readline()を実行すると、バイナリファイル全体が単一の文字列に読み込まれ、ヒープスペースでOutOfMemoryが発生します。

次に、簡単なJDKの代替案を示します。

public static void main(String[] args) throws Exception
{
    byte[] array = new byte[1024];
    FileInputStream fis = new FileInputStream(new File("<Path-to-input-file>"));
    FileOutputStream fos = new FileOutputStream(new File("<Path-to-output-file>"));
    int length = 0;
    while((length = fis.read(array)) != -1)
    {
        fos.write(array, 0, length);
    }
    fis.close();
    fos.close();
}

注意事項:

  • 上記の例では、1Kバイトのバッファーを使用してファイルをコピーします。ただし、ネットワーク経由でこのコピーを実行している場合は、バッファサイズを微調整することができます。

  • FileChannel または Commons IO のようなライブラリを使用する場合は、実装が上記のようなものになることを確認してください

1
Chris

これは問題なく機能しました。

    char charArray[] = new char[ MAX_BUFFER_SIZE ];
    int i = 0;
    int c = 0;
    while((c = br.read()) != -1 && i < MAX_BUFFER_SIZE) {
        char character = (char) c;
        charArray[i++] = character;
   }
   return Arrays.copyOfRange(charArray,0,i); 
0
Dileepa

Apache httpCoreの下にEntityUtilsクラスがあります。このクラスのgetString()メソッドを使用して、応答コンテンツから文字列を取得します。

0
Sanjeev

Apache Commons IO FileUtils。 FileUtilsクラスでは非常にシンプルで、いわゆるDOS攻撃は最上層から直接来ないので、他の選択肢は考えられません。ファイルを書くのはとても簡単です

String content =FileUtils.readFileToString(new File(filePath));

これについて詳しく調べることができます。

0
Kris