Maneira mais robusta de ler um arquivo ou stream usando Java (para evitar ataques DoS)

Atualmente eu tenho o código abaixo para ler um inputStream. Eu estou armazenando o arquivo inteiro em uma variável StringBuilder e processando essa seqüência depois.

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

O código foi revisado pela equipe de segurança e os seguintes comentários foram recebidos:

  1. BufferedReader.readLine é suscetível a ataques do DOS (negação de serviço) (linha de comprimento de infeção, arquivo enorme que não contém alimentação de linha / ação)

  2. Esgotamento de resources para a variável StringBuilder (casos em que um arquivo contendo dados maiores que a memory avaialble)

Abaixo estão as soluções que eu poderia pensar:

  1. Crie uma implementação alternativa do método readLine ( readLine(int limit) ), que verifica o não. de bytes lidos e se exceder o limite especificado, lançar uma exceção personalizada.

  2. Processe o arquivo linha por linha, sem carregar o arquivo na íntegra. (solução não-java pura :))

Por favor, sugira se existem bibliotecas que implementam as soluções acima. Sugira também quaisquer soluções alternativas que ofereçam mais robustez ou sejam mais convenientes para implementar do que as propostas. Embora o desempenho também seja um requisito importante, a segurança vem em primeiro lugar.

Desde já, obrigado.

Resposta Atualizada

Você quer evitar todos os tipos de ataques do DOS (em linhas, no tamanho do arquivo, etc). Mas no final da function, você está tentando converter o arquivo inteiro em uma única String ! Suponha que você limite a linha a 8 KB, mas o que acontece se alguém lhe enviar um arquivo com duas linhas de 8 KB? A parte de leitura de linha irá passar, mas quando finalmente você combinar tudo em uma única string, a String irá sufocar toda a memory disponível.

Então, desde que você finalmente está convertendo tudo em uma única String, limitar o tamanho da linha não importa, nem é seguro. Você tem que limitar o tamanho inteiro do arquivo.

Em segundo lugar, o que você está basicamente tentando fazer é tentar ler dados em partes. Então você está usando o BufferedReader e lendo-o linha por linha. Mas o que você está tentando fazer e o que você realmente quer no final – é uma maneira de ler o arquivo peça por peça. Em vez de ler uma linha por vez, por que não ler 2 KB de cada vez?

BufferedReader – pelo seu nome – tem um buffer dentro dele. Você pode configurar esse buffer. Vamos supor que você crie um BufferedReader com tamanho de buffer de 2 KB:

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

Agora, se o InputStream que você passa para o BufferedReader tiver 100 KB de dados, o BufferedReader lerá automaticamente 2 KB de cada vez. Então, ele lerá o stream 50 vezes, 2 KB cada (50x2KB = 100 KB). Da mesma forma, se você criar BufferedReader com um tamanho de buffer de 10 KB, ele lerá a input 10 vezes (10×10 KB = 100 KB).

BufferedReader já faz o trabalho de ler o seu arquivo chunk-by-chunk. Então você não quer adicionar uma camada extra de linha por linha acima dela. Apenas foque no resultado final – se o seu arquivo no final for muito grande (> RAM disponível) – como você irá convertê-lo em uma String no final?

Uma maneira melhor é simplesmente passar as coisas como um CharSequence . Isso é o que o Android faz. Ao longo das APIs do Android, você verá que eles retornam o CharSequence todos os lugares. Como o StringBuilder também é uma subclass de CharSequence , o Android usará internamente um String , ou um StringBuilder ou alguma outra class de string otimizada baseada no tamanho / natureza da input. Assim, você poderia retornar diretamente o próprio object StringBuilder depois de ler tudo, em vez de convertê-lo em uma String . Isso seria mais seguro contra dados grandes. StringBuilder também mantém o mesmo conceito de buffers dentro dele, e alocará internamente vários buffers para strings grandes, em vez de uma string longa.

Então, no geral:

  • Limite o tamanho geral do arquivo, pois você vai lidar com todo o conteúdo em algum momento. Esqueça-se de limitar ou dividir linhas
  • Leia em pedaços

Usando o Apache Commons IO, aqui está como você leria os dados de um BoundedInputStream em um StringBuilder , dividindo por blocos de 2 KB em vez de linhas:

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

Resposta Original

Use BoundedInputStream da biblioteca de IO do Apache Commons . Seu trabalho fica muito mais fácil.

O código a seguir fará o que você deseja:

 public static String getContentFromInputStream(InputStream inputStream) { inputStream = new BoundedInputStream(inputStream, ); // Rest code are all same 

Você simplesmente envolve seu InputStream com um BoundedInputStream e especifica um tamanho máximo. BoundedInputStream cuidará de limitar as leituras até o tamanho máximo.

Ou você pode fazer isso quando estiver criando o leitor:

 BufferedReader bufferedReader = new BufferedReader( new InputStreamReader( new BoundedInputStream(inputStream, ) ) ); 

Basicamente, o que estamos fazendo aqui é que limitamos o tamanho de leitura na própria camada InputStream , em vez de fazê-lo ao ler linhas. Então você acaba com um componente reutilizável como BoundedInputStream que limita a leitura na camada InputStream, e você pode usá-lo onde quiser.

Editar: nota de rodapé adicionada

Editar 2: Resposta atualizada adicionada com base nos comentários

Existem basicamente quatro maneiras de fazer o processamento de arquivos:

  1. Processamento baseado em stream (o modelo java.io.InputStream ): Opcionalmente coloque um bufferedReader ao redor do stream, repita e leia o próximo texto disponível do stream (se nenhum texto estiver disponível, bloqueie até que algum fique disponível), processe cada parte texto de forma independente, como é lido (catering para tamanhos variados de peças de texto)

  2. Processamento Não Bloqueado Baseado em Chunk (o modelo java.nio.channels.Channel ): Cria um conjunto de buffers de tamanho fixo (representando os “chunks” a serem processados), lidos em cada um dos buffers, por sua vez, sem bloqueio (nio Delegação de API para IO nativo, usando threads O / S rápidas), seu thread de processamento principal seleciona cada buffer, uma vez preenchido, e processa o fragment de tamanho fixo, já que outros buffers continuam sendo carregados assincronamente.

  3. Processamento de arquivo de peça (incluindo processamento linha por linha) (pode alavancar (1) ou (2) para isolar ou construir cada “parte”): quebrar seu formato de arquivo em sub-partes semanticamente significativas (se possível! linhas podem ser possíveis!), iterar através de partes de stream ou pedaços e construir conteúdo na unidade de memory. A próxima parte é completamente construída, processe cada parte assim que for construída.

  4. Processamento de arquivo inteiro (o modelo java.nio.file.Files ): Leia o arquivo inteiro na memory em uma operação, processe o conteúdo completo

Qual deles você deve usar?
Depende – do conteúdo do seu arquivo e do tipo de processamento que você precisa.
Do ponto de vista da eficiência do uso de resources (do melhor para o pior), é: 1,2,3,4.
A partir de uma perspectiva de velocidade e eficiência de processamento (do melhor para o pior) é: 2,1,3,4.
De uma perspectiva de programação fácil (do melhor para o pior): 4,3,1,2.
No entanto, alguns tipos de processamento podem exigir mais do que o menor texto (excluindo 1 e talvez 2) e alguns formatos de arquivo podem não ter partes internas (descartando 3).

Você está fazendo 4. Eu sugiro que você mude para 3 (ou menos), se puder .

Em 4, há apenas uma maneira de evitar o DOS – limite o tamanho antes que ele seja lido na memory (ou, nesse caso, copiado para o seu sistema de arquivos). É tarde demais, uma vez que é lido. Se isso não for possível, tente 3, 2 ou 1.

Limitando o tamanho do arquivo

Muitas vezes, o arquivo é enviado por meio de um formulário HTML.

Se você fizer o upload usando a anotação Servlet @MultipartConfig e request.getPart().getInputStream() , terá controle sobre a quantidade de dados que você leu no stream. Além disso, request.getPart().getSize() retorna o tamanho do arquivo antecipadamente e, se for pequeno o suficiente, você pode fazer request.getPart().write(path) para gravar o arquivo no disco.

Se estiver carregando usando o JSF, o JSF 2.2 (muito novo) componente html padrão ( javax.faces.component.html.InputFile ), que possui um atributo para maxLength ; As implementações anteriores ao JSF 2.2 possuem componentes customizados semelhantes (por exemplo, o Tomahawk possui com o atributo maxLength; o PrimeFaces tem o atributo com sizeLimit ).

Alternativas para ler o arquivo inteiro

Seu código que usa InputStream , StringBuilder , etc, é uma maneira eficiente de ler o arquivo inteiro, mas não é necessariamente a maneira mais simples (menos linhas de código).

Desenvolvedores júnior / médios poderiam entender erroneamente que você está fazendo um processamento eficiente baseado em stream, quando você está processando o arquivo inteiro – portanto, inclua comentários apropriados.

Se você quiser menos código, tente um dos seguintes procedimentos:

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

Mas eles exigem cuidado ou podem ser ineficientes no uso de resources. Se você usar readAllLines e concatenar os elementos List em uma única String , consumiria o dobro da memory (para os elementos List + a String concatenada). Da mesma forma, se você usar readAllBytes , seguido por codificação para String ( new String(byteContents, charset) ), então, novamente, você está usando “double” a memory. Portanto, é melhor processar diretamente em List ou byte[] , a menos que você limite seus arquivos a um tamanho pequeno o suficiente.

em vez de readLine, use read, que lê uma determinada quantidade de chars.

em cada loop, verifique quantos dados foram lidos, se é mais do que uma certa quantia, mais que o máximo de uma input esperada, pare e retorne um erro e registre-o.

Uma observação adicional, notei que você não fechou seu BufferedInputStream. Você deve fechar seu BufferedReader finally bloco como isso é suscetível a vazamentos de memory.

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

Não há necessidade de fechar explicitamente o new InputStreamReader(inputStream) pois ele será fechado automaticamente quando você chamar para fechar a class de quebra bufferedReader

Eu enfrentei um problema semelhante ao copiar um arquivo binário enorme (que geralmente não contém caracteres de nova linha). fazer um readline () leva a ler todo o arquivo binário em uma única string, causando OutOfMemory no espaço Heap.

Aqui está uma alternativa simples do JDK:

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

Coisas para observar:

  • O exemplo acima copia o arquivo usando um buffer de 1K bytes. No entanto, se você estiver fazendo essa cópia pela rede, convém ajustar o tamanho do buffer.

  • Se você gostaria de usar o FileChannel ou bibliotecas como o Commons IO , apenas certifique-se de que a implementação se resume a algo como acima

Eu não posso pensar em uma solução diferente de Apache Commons IO FileUtils. É bem simples com a class FileUtils, já que o chamado ataque DOS não virá diretamente da camada superior. Ler e escrever um arquivo é muito simples, pois você pode fazer isso com apenas uma linha de código, como

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

Você pode explorar mais sobre isso.

Há a class EntityUtils sob o httpCore do Apache. Use o método getString () dessa class para obter o conteúdo String de Response.

Isso funcionou para mim sem problemas.

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