BufferedReader 的安全实现

Safe implementation of BufferdReader

我想使用 BufferedReader 读取上传到我的服务器的文件。

该文件将被写成 CSV 文件,但我不能假设这一点,所以我编写了一些测试,其中文件是图像或二进制文件(假设客户端向我发送了错误的文件或攻击者试图破坏我的服务),或者更糟的是,该文件是一个有效的 CSV 文件,但有一行 100MB。

我的应用程序可以处理这个问题,但它必须读取文件的第一行:

...
String firstLine = bufferedReader.readLine();
//Perform some validations and reject the file if it's not a CSV file
...

但是,当我编写一些测试代码时,我发现了一个潜在的风险:BufferedReader 在找到 return 行之前不会对其读取的字节数执行任何控制,因此它可以结束抛出 OutOfMemoryError。

这是我的测试:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import org.junit.Test;

public class BufferedReaderTest {

    @Test(expected=OutOfMemoryError.class)
    public void testReadFileWithoutReturnLineCharacter() throws IOException {
        BufferedReader bf = new BufferedReader(getInfiniteReader());

        bf.readLine();

        bf.close();
    }

    private Reader getInfiniteReader() {
        return new Reader(){

            @Override
            public int read(char[] cbuf, int off, int len) throws IOException {
                return 'A';
            }

            @Override
            public void close() throws IOException {

            }
        };
    }
}

我一直在互联网上查找一些安全的 BufferedReader 实现,但找不到任何东西。我发现的唯一 class 是来自 apache IO 的 BoundedInputStream,它限制了输入流读取的字节数。

我需要一个 BufferedReader 的实现,它知道如何限制每行 bytes/characters 读取 的数量

像这样:

有人知道具有这种行为的 BufferedReader 实现吗?

这不是您应该继续检测文件是否为二进制文件的方式。

以下是检查文件是否真正为文本的方法;请注意,这需要您事先知道编码:

final Charset cs = StandardCharsets.UTF_8; // or another

final CharsetDecoder decoder = cs.newDecoder()
    .onMalformedInput(CodingErrorAction.REPORT); // default is REPLACE!

// Here, "in" is the input stream from the file
try (
    final Reader reader = new InputStreamReader(in, decoder);
) {
    final char[] buf = new char[4096]; // or other size
    while (reader.read(buf) != -1)
        ; // nothing
} catch (MalformedInputException e) {
    // cannot decode; binary, or wrong encoding
}

现在,由于您可以在 Reader 上初始化 BufferedReader,您可以使用:

try (
    final Reader r = new InputStreamReader(in, decoder);
    final BufferedReader reader = new BufferedReader(r);
) {
    // Read lines normally
} catch (CharacterCodingException e) {
    // Not a CSV, it seems
}

// etc

现在,稍微解释一下它是如何工作的……虽然这是阅读 Java 中文本的基本部分,但它也是同样被根本性误解的部分!

当您使用 Reader 以文本形式读取文件时,您必须指定字符编码;在 Java 中,这是 Charset.

内部发生的是 Java 将从 Charset 创建一个 CharsetDecoder,读取 byte 流并输出 char 流。并且有3种处理错误的方法:

  • CodingErrorAction.REPLACE默认值):不可映射的字节序列被替换为Unicode replacement character(它确实敲响了警钟,对吧?);
  • CodingErrorAction.IGNORE:不可映射的字节序列不会触发 char;
  • 的发射
  • CodingErrorAction.REPORT:不可映射的字节序列触发抛出一个CharacterCodingException,继承了IOException;反过来,CharacterCodingException 的两个子类是 MalformedInputExceptionUnmappableCharacterException.

因此,要检测文件是否为真正的文本,您需要做的是:

  • 事先知道编码!
  • 使用 CharsetDecoder 配置 CodingErrorAction.REPORT;
  • InputStreamReader.
  • 中使用它

这是一种方式;还有其他人。然而,他们所有人都会在某个时候使用 CharsetDecoder

同样,还有一个CharsetEncoder用于反向操作(char流到byte流),这就是Writer家族所使用的。

谢谢@fge 的回答。我最终实现了一个安全的 Reader 可以处理行太长(或根本没有行)的文件。

如果有人想看代码,可以在这里找到项目(非常小的项目,但有很多测试):

https://github.com/jfcorugedo/security-io