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

时间:2013-06-13 10:22:31

标签: java bufferedreader readline denial-of-service

目前,我有以下代码来阅读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(拒绝服务)攻击(无限长行,不包含换行/回车的大文件)

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

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

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

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

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

8 个答案:

答案 0 :(得分:35)

更新答案

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

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

其次,你基本上要做的是,你试图以块的形式读取数据。所以你正在使用BufferedReader并逐行阅读。但是你要做的是什么,以及你最终想要的是什么 - 是一种逐段阅读文件的方式。而不是一次只读一行,为什么不一次读取2 KB?

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

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

现在,如果传递给InputStream的{​​{1}}有100 KB的数据,BufferedReader会自动将其读取为2 KB。因此它将读取流50次,每次2 KB(50x2KB = 100 KB)。同样,如果您创建具有10 KB缓冲区大小的BufferedReader,它将读取输入10次(10x10KB = 100 KB)。

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

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

总的来说:

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

使用Apache Commons IO,以下是将StringBuilder中的数据读入BoundedInputStream的方法,将2 KB块除以行分割:

StringBuilder

原始答案

使用BoundedInputStream库中的Apache Commons IO。你的工作变得更加容易。

以下代码将执行您想要的操作:

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

您只需将public static String getContentFromInputStream(InputStream inputStream) { inputStream = new BoundedInputStream(inputStream, <number-of-bytes>); // Rest code are all same InputStream包裹,然后指定最大尺寸。 BoundedInputStream将负责将读数限制为最大尺寸。

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

BoundedInputStream

基本上我们在这里做的是,我们限制BufferedReader bufferedReader = new BufferedReader( new InputStreamReader( new BoundedInputStream(inputStream, <no-of-bytes>) ) ); 层本身的读取大小,而不是在读取行时这样做。因此,最终得到一个可重用的组件,如InputStream,它限制了InputStream层的读取,你可以在任何你想要的地方使用它。

编辑:添加了脚注

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

答案 1 :(得分:14)

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

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

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

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

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

  5. 你应该使用哪一个?
    这取决于您的文件内容和您需要的处理类型 从资源利用效率的角度(从最好到最差)是:1,2,3,4 从处理速度&amp;效率观点(从最好到最差)是: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组件<h:inputFile>javax.faces.component.html.InputFile),其具有maxLength的属性; JSF 2.2之前的实现具有类似的自定义组件(例如,Tomahawk具有<t:InputFileUpload> maxLength属性; PrimeFaces具有<p:FileUpload>属性sizeLimit

    阅读整个文件的替代方法

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

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

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

     List<String> stringList = java.nio.file.Files.readAllLines(path, charset);
    
     or 
    
     byte[] byteContents =  java.nio.file.Files.readAllBytes(path);
    

    但它们需要小心,否则它们在资源使用方面效率低下。如果您使用readAllLines然后将List元素连接到单个String,那么您将消耗双倍的内存(对于List元素+连接的{{1} })。同样,如果您使用String,然后编码为readAllBytesString),那么您再次使用“双倍”内存。因此,最好直接针对new String(byteContents, charset)List<String>进行处理,除非您将文件限制为足够小的尺寸。

答案 2 :(得分:3)

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

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

答案 3 :(得分:1)

在复制巨大的二进制文件(通常不包含换行符)时遇到了类似的问题。执行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字节的缓冲区复制文件。但是,如果您通过网络进行此复制,则可能需要调整缓冲区大小。

  • 如果您想使用FileChannelCommons IO之类的图书馆,请确保实施归结为上述内容

答案 4 :(得分:0)

除了Apache Commons IO FileUtils.之外,我无法想到一个解决方案 它与FileUtils类非常简单,因为所谓的DOS攻击不会直接来自顶层。 读取和写入文件非常简单,因为只需使用一行代码(如

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

您可以详细了解这一点。

答案 5 :(得分:0)

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

答案 6 :(得分:0)

这对我没有任何问题。

var retval={};
data_model.select_company(index,retval);
console.log(retval.company_name);
console.log(retval.identity_id);

答案 7 :(得分:0)

来自 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();