使用 Java 读取文件或流的最强大的方法(以防止 DoS 攻击)

Posted

技术标签:

【中文标题】使用 Java 读取文件或流的最强大的方法(以防止 DoS 攻击)【英文标题】:Most Robust way of reading a file or stream using Java (to prevent DoS attacks) 【发布时间】:2013-06-09 16:42:39 【问题描述】:

目前我有以下代码用于读取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();


安全团队对代码进行了审查,并收到了以下 cmets:

    BufferedReader.readLine 容易受到 DOS(拒绝服务)攻击(无限长的行,不包含换行/回车的大文件)

    StringBuilder 变量的资源耗尽(文件包含的数据大于可用内存的情况)

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

    创建 readLine 方法 (readLine(int limit)) 的替代实现,用于检查编号。读取的字节数,如果超过指定限制,则抛出自定义异常。

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

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

【问题讨论】:

使用阅读器时始终设置字符编码 最健壮的方法是使用来自 apache commons 或更高级别库的 IOUTils。 或者换一种方式,实现较低级别的字节读取并随时监控大小。配置您的限制并以适合该方法的合同的任何方式处理长度违规。 @domfarr :这正是我计划在新的 readLine 方法的实现中包含的内容。 @UnniKris 请注意,即使您逐行阅读,当用户为您提供没有换行符的 2GB 文件时,您也会受到 DOS 攻击。 【参考方案1】:

而不是 readLine 使用 read 读取给定数量的字符。

在每个循环中检查已经读取了多少数据,如果超过一定数量,超过预期输入的最大值,停止它并返回错误并记录它。

【讨论】:

+1,但是 read 方法不会让我灵活地逐行阅读,否则我将不得不在代码中添加另一个换行/回车检查。你知道任何现有的图书馆吗? @UnniKris,是的,Apache 公共库也是如此,请查看下面的说明以了解如何使用它...【参考方案2】:

更新答案

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

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

其次,您基本上想要做的是,您正在尝试以块的形式读取数据。因此,您正在使用 BufferedReader 并逐行阅读。但是你想要做的,以及你最后真正想要的 - 是一种逐个读取文件的方式。与其一次读取一行,不如一次读取 2 KB?

BufferedReader——顾名思义——里面有一个缓冲区。您可以配置该缓冲区。假设您创建了一个缓冲区大小为 2 KB 的 BufferedReader

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

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

BufferedReader 已经完成了逐块读取文件的工作。所以你不想在它上面逐行添加一个额外的层。只关注最终结果 - 如果最后的文件太大(> 可用 RAM) - 最后你将如何将其转换为 String

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

总的来说:

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

使用 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

您只需用BoundedInputStream 包裹您的InputStream 并指定最大大小。 BoundedInputStream 将负责将读取限制为最大大小。

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

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

基本上我们在这里所做的是,我们在InputStream 层本身限制读取大小,而不是在读取行时这样做。所以你最终得到了一个像BoundedInputStream 这样的可重用组件,它限制了 InputStream 层的读取,你可以在任何你想要的地方使用它。

编辑:添加脚注

编辑 2:添加了基于 cmets 的更新答案

【讨论】:

+1,不过有一个疑问。 BoundedInputStream 限制是否适用于正在读取的行或整个文件内容? 不适用于代表整个文件的 InputStream。因此,您将整个文件的 InputStream 限制在一定范围内。 谢谢。虽然 BoundedInputStream 将阻止文件大小的 DoS,但在我提到的第一种情况下它仍然容易受到 DoS 攻击(无限长的行,不包含换行/回车的大文件) @UnniKris 更新了答案并添加了一个全新的部分。我想把它作为一个单独的答案发布,但只是放在这里,因为它更容易查看 您发布的内容是正确的,但您似乎误解了我的问题。我想对行大小和文件大小都有限制,两者都是不同的。例如:我想将行大小限制为 1000 个字符,文件大小限制为 5 MB。我给出的代码是我想要解决方案的代码。它不是解决方案本身。我已修改代码以包含描述预期行为的 cmets。感谢您的回复。【参考方案3】:

文件处理基本上有4种方式:

    基于流的处理java.io.InputStream 模型):可选择在流周围放置一个 bufferedReader,迭代并从流中读取下一个可用文本(如果没有可用文本, 阻塞直到一些可用),在阅读时独立处理每段文本(适应各种大小的文本段)

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

    部分文件处理(包括逐行处理)(可以利用 (1) 或 (2) 来隔离或构建每个“部分”):打破文件格式分解成语义上有意义的子部分(如果可能的话!分行是可能的!),遍历流片段或块并在内存中建立内容,直到下一部分完全构建,一旦构建就处理每个部分。

    整个文件处理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 @MultipartConfig 注释和 request.getPart().getInputStream() 上传,您可以控制从流中读取的数据量。此外,request.getPart().getSize() 会提前返回文件大小,如果足够小,您可以通过request.getPart().write(path) 将文件写入磁盘。

如果使用 JSF 上传,那么 JSF 2.2(非常新)有标准的 html 组件&lt;h:inputFile&gt; (javax.faces.component.html.InputFile),它有一个maxLength 的属性; JSF 2.2 之前的实现具有类似的自定义组件(例如,Tomahawk 具有 &lt;t:InputFileUpload&gt;maxLength 属性;PrimeFaces 具有 &lt;p:FileUpload&gt;sizeLimit 属性)。

读取整个文件的替代方法

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

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

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

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

 or 

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

但它们需要小心,否则它们在资源使用方面可能效率低下。如果您使用readAllLines,然后将List 元素连接成一个String,那么您将消耗双倍的内存(对于List 元素+ 连接的String)。同样,如果您使用readAllBytes,然后编码为Stringnew String(byteContents, charset)),那么您再次使用“双”内存。因此最好直接针对List&lt;String&gt;byte[] 进行处理,除非您将文件限制为足够小的大小。

【讨论】:

【参考方案4】:

我想不出除了Apache Commons IO FileUtils.之外的解决方案 FileUtils 类非常简单,因为所谓的 DOS 攻击不会直接来自顶层。 读取和写入文件非常简单,只需一行代码即可完成,例如

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

您可以对此进行更多探索。

【讨论】:

【参考方案5】:

我在复制一个巨大的二进制文件(通常不包含换行符)时遇到了类似的问题。执行 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,虽然这样可以防止 DoS 攻击,但它不能满足我逐行处理的需要。 对二进制文件执行readLine() 是错误的,原因有很多,但问题是关于readLine(),而不是复制二进制数据。【参考方案6】:

Apache httpCore 下有 EntityUtils 类。使用该类的getString()方法从Response内容中获取String。

【讨论】:

【参考方案7】:

这对我来说没有任何问题。

    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); 

【讨论】:

【参考方案8】:

来自 Fortify Scan 的建议。您可以将InputStream 调整为其他资源,例如HTTP request InputStream

InputStream zipInput = zipFile.getInputStream(zipEntry);
Reader zipReader = new InputStreamReader(zipInput);
BufferedReader br = new BufferedReader(zipReader);
StringBuffer sb = new StringBuffer();
int intC;
while ((intC = br.read()) != -1)
    char c = (char)intC;
    if (c == "\n")
       break;
    
    if (sb.length >= MAX_STR_LEN)
       throw new Exception("Input too long");
    
    sb.append(c);

String line = sb.toString();

【讨论】:

以上是关于使用 Java 读取文件或流的最强大的方法(以防止 DoS 攻击)的主要内容,如果未能解决你的问题,请参考以下文章

java.io.EOFException这是个啥异常应该怎么解决

java 以二进制流的方式读取mysql 中的blob文件,并写入本地文件夹下

读写文件的基本流都有哪些

java-关于文件操作-输出流的使用

JAVA-基础(字符流)

java非常好用的读取文件的流的代码