使用 Java 读取文件或流的最可靠方式(防止 DoS 攻击)

目前我有下面的代码来阅读.我将整个文件存储到一个变量中,然后处理此字符串。InputStreamStringBuilder

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(拒绝服务)攻击(无限长的行,不包含换行/回车符的大文件)

  2. 变量的资源耗尽(当文件包含的数据大于可用内存时)StringBuilder

以下是我能想到的解决方案:

  1. 创建方法 () 的替代实现,用于检查 no。读取的字节数,如果超过指定的限制,则引发自定义异常。readLinereadLine(int limit)

  2. 逐行处理文件,而不完整加载文件。(纯非Java解决方案:))

请建议是否有任何现有的库实现上述解决方案。还要建议任何比建议的更健壮或更方便实现的替代解决方案。虽然性能也是一项主要要求,但安全性是第一位的。


答案 1

更新的答案

您希望避免各种DOS攻击(在行上,文件大小上等)。但是在函数结束时,您尝试将整个文件转换为单个!!!假设您将行限制为 8 KB,但是如果有人向您发送两个 8 KB 行的文件,会发生什么情况?行读取部分将通过,但当您最终将所有内容组合成单个字符串时,字符串将阻塞所有可用内存。String

因此,由于您最终将所有内容转换为单个字符串,因此限制行大小并不重要,也不安全。您必须限制文件的整个大小。

其次,你基本上试图做的是,你试图以块的形式读取数据。因此,您正在逐行使用和阅读它。但是,您要执行的操作以及最后真正想要的操作是逐个读取文件的某种方式。为什么不一次读取一行,而是一次读取 2 KB?BufferedReader

BufferedReader- 按其名称 - 内部有一个缓冲区。您可以配置该缓冲区。假设您创建了一个缓冲区大小为 2 KB 的函数:BufferedReader

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

现在,如果您传递给的数据有100 KB,则一次会自动读取2 KB的数据。因此,它将读取流 50 次,每个 2 KB(50x2KB = 100 KB)。同样,如果使用 10 KB 缓冲区大小进行创建,它将读取输入 10 次 (10x10KB = 100 KB)。InputStreamBufferedReaderBufferedReaderBufferedReader

BufferedReader已经完成了逐块读取文件的工作。因此,您不希望在其上方逐行添加额外的图层。只需关注最终结果 - 如果您的文件在最后太大(>可用的RAM) - 您将如何将其转换为最后?String

一种更好的方法是将事情作为.这就是Android所做的。在整个Android API中,您将看到它们无处不在。由于 也是 的子类,Android 将在内部使用 一个 、或一个或一些其他优化的字符串类,基于输入的大小/性质。因此,您可以在读取所有内容后直接返回对象本身,而不是将其转换为 .这对大数据更安全。 它还保持其内部缓冲区的相同概念,它将在内部为大字符串分配多个缓冲区,而不是一个长字符串。CharSequenceCharSequenceStringBuilderCharSequenceStringStringBuilderStringBuilderStringStringBuilder

所以总的来说:

  • 限制整体文件大小,因为您将在某个时候处理整个内容。忘记限制或分割行
  • 以块为单位读取

使用Apache Commons IO,以下是您将如何将数据从 a 读取到 一个 中,按 2 KB 块而不是行进行拆分:BoundedInputStreamStringBuilder

// 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

您只需用 a 简单地包装,然后指定最大尺寸即可。 将负责将读取限制为最大大小。InputStreamBoundedInputStreamBoundedInputStream

或者,您可以在创建读取器时执行此操作:

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

基本上,我们在这里所做的是,我们限制了层本身的读取大小,而不是在读取行时这样做。因此,您最终会得到一个可重用的组件,例如限制输入流层的读数,并且可以在任何地方使用它。InputStreamBoundedInputStream

编辑:添加脚注

编辑2:根据评论添加了更新的答案


答案 2

基本上有4种方法可以进行文件处理:

  1. 基于流的处理(模型):可以选择在流周围放置一个缓冲的Reader,迭代和读取流中的下一个可用文本(如果没有可用的文本,则阻止直到某些文本可用),在读取时独立处理每段文本(满足大小不一的文本块)java.io.InputStream

  2. 基于块的非阻塞处理(模型):创建一组固定大小的缓冲区(表示要处理的“块”),依次读入每个缓冲区而不会阻塞(nio API委托给本机IO,使用快速O / S级线程),您的主处理线程在填充后依次选择每个缓冲区并处理固定大小的块, 因为其他缓冲区继续异步加载。java.nio.channels.Channel

  3. 部件文件处理(包括逐行处理)(可以利用 (1) 或 (2) 来隔离或构建每个“部件”):将文件格式分解为语义上有意义的子部件(如果可能的话!可以分成行!),循环访问流段或块并在内存中构建内容,直到下一个部分完全构建,在构建后立即处理每个部分。

  4. 整个文件处理(模型):一次操作将整个文件读入内存,处理完整的内容java.nio.file.Files

您应该使用哪一个?
这取决于 - 您的文件内容和您需要的处理类型。
从资源利用效率的角度来看(从最好到最差)是:1,2,3,4。
从处理速度和效率的角度来看(从最好到最差)是:2,1,3,4。
从编程的易用性角度来看(从最好到最差):4,3,1,2。
但是,某些类型的处理可能需要超过最小的文本片段(排除 1,也可能排除 2),并且某些文件格式可能没有内部部分(排除 3)。

你正在做4。如果可以的话,我建议你改到3(或更低)。

在 4 下,只有一种方法可以避免 DOS - 在将其读入内存(或复制到文件系统)之前限制大小。一旦读进去就太晚了。如果无法做到这一点,请尝试 3、2 或 1。

限制文件大小

通常,文件是通过 HTML 表单上传的。

如果使用 Servlet 注释 和 上传,则可以控制从流中读取的数据量。此外,提前返回文件大小,如果它足够小,则可以将文件写入磁盘。@MultipartConfigrequest.getPart().getInputStream()request.getPart().getSize()request.getPart().write(path)

如果使用 JSF 上传,则 JSF 2.2(非常新)具有标准的 html 组件 (),该组件具有 ;JSF 2.2之前的实现具有类似的自定义组件(例如,战斧具有属性;PrimeFaces具有属性)。<h:inputFile>javax.faces.component.html.InputFilemaxLength<t:InputFileUpload>maxLength<p:FileUpload>sizeLimit

读取整个文件的替代方法

使用 等的代码是读取整个文件的有效方法,但不一定是最简单的方法(最少的代码行)。InputStreamStringBuilder

初级/普通开发人员可能会在处理整个文件时误以为您正在进行高效的基于流的处理 - 因此请包含适当的注释。

如果您想要更少的代码,可以尝试以下方法之一:

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

 or 

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

但它们需要护理,否则资源使用效率可能低下。如果使用元素,然后将元素串联成单个 ,则将消耗双倍的内存(对于元素 + 串联的元素)。同样,如果您使用 ,后跟编码为 (),那么同样,您使用的是“双倍”内存。因此,最好直接针对 或 进行处理,除非您将文件限制为足够小的大小。readAllLinesListStringListStringreadAllBytesStringnew String(byteContents, charset)List<String>byte[]


推荐