web-dev-qa-db-fra.com

Le moyen le plus robuste de lire un fichier ou un flux en utilisant Java (pour empêcher les attaques par déni de service)

Actuellement, j'ai le code ci-dessous pour lire une InputStream. Je stocke le fichier entier dans une variable StringBuilder et traite cette chaîne par la suite.

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

}

Le code a été examiné par l'équipe de sécurité et les commentaires suivants ont été reçus:

  1. BufferedReader.readLine est sujet aux attaques de type DOS (déni de service) (ligne de longueur infinie, fichier énorme ne contenant pas de retour à la ligne ni de retour chariot)

  2. Épuisement des ressources pour la variable StringBuilder (cas où un fichier contient plus de données que la mémoire disponible)

Voici les solutions auxquelles je pourrais penser: 

  1. Créez une autre implémentation de la méthode readLine (readLine(int limit)), qui recherche le no. d'octets lus et s'il dépasse la limite spécifiée, lève une exception personnalisée.

  2. Traitez le fichier ligne par ligne sans charger le fichier en entier. (solution pure non-Java :))

Indiquez, le cas échéant, les bibliothèques implémentant les solutions ci-dessus . Suggérez également toute solution de remplacement plus robuste ou plus pratique à mettre en œuvre que celles proposées. Bien que les performances constituent également une exigence majeure, la sécurité est une priorité.

17
Unni Kris

Réponse mise à jour

Vous voulez éviter toutes sortes d'attaques DOS (sur les lignes, sur la taille du fichier, etc.). Mais à la fin de la fonction, vous essayez de convertir le fichier entier en une seule variable String !!! Supposons que vous limitiez la ligne à 8 Ko, mais que se passe-t-il si quelqu'un vous envoie un fichier contenant deux lignes de 8 Ko? La partie de lecture de ligne passera, mais lorsque vous combinerez finalement tout dans une seule chaîne, la chaîne épongera toute la mémoire disponible.

Donc, puisque finalement vous convertissez tout en une seule chaîne, limiter la taille de la ligne n'a pas d'importance et n'est pas sûr. Vous devez limiter la taille totale du fichier.

Deuxièmement, vous essayez essentiellement de lire les données en morceaux. Donc, vous utilisez BufferedReader et vous le lisez ligne par ligne. Mais ce que vous essayez de faire et ce que vous voulez vraiment à la fin, c’est un moyen de lire le dossier pièce par pièce. Au lieu de lire une ligne à la fois, pourquoi ne pas lire 2 Ko à la fois?

BufferedReader - par son nom - contient un tampon. Vous pouvez configurer ce tampon. Supposons que vous créez une BufferedReader avec une taille de mémoire tampon de 2 Ko:

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

Maintenant, si la InputStream que vous passez à BufferedReader a 100 Ko de données, BufferedReader le lira automatiquement 2 Ko à la fois. Ainsi, il lira le flux 50 fois, 2 Ko chacun (50 x 2 Ko = 100 Ko). De même, si vous créez BufferedReader avec une taille de mémoire tampon de 10 Ko, il lira l’entrée 10 fois (10 x 10 Ko = 100 Ko).

BufferedReader fait déjà le travail de lecture de votre fichier morceau par morceau. Donc, vous ne voulez pas ajouter une couche supplémentaire de ligne par ligne au-dessus. Concentrez-vous simplement sur le résultat final - si votre fichier à la fin est trop volumineux (> RAM disponible) - comment allez-vous le convertir en un String à la fin?

Un meilleur moyen consiste simplement à transmettre les éléments sous forme de CharSequence. C'est ce que fait Android. Tout au long des API Android, vous verrez qu'elles renvoient CharSequence partout. Étant donné que StringBuilder est également une sous-classe de CharSequence, Android utilisera en interne soit une String, ou une StringBuilder ou une autre classe de chaîne optimisée en fonction de la taille/nature de l'entrée. Vous pouvez donc retourner directement l'objet StringBuilder lui-même une fois que vous avez tout lu, plutôt que de le convertir en String. Ce serait plus sûr contre les grandes données. StringBuilder conserve également le même concept de mémoire tampon à l'intérieur, et il allouera en interne plusieurs mémoires tampons pour les chaînes volumineuses, plutôt qu'une seule chaîne longue.

Donc globalement:

  • Limitez la taille globale du fichier, car vous allez gérer tout le contenu à un moment donné. Oubliez les lignes de limitation ou de division
  • Lire en morceaux

À l'aide d'Apache Commons IO, voici comment lire les données d'une BoundedInputStream dans une StringBuilder, en séparant par blocs de 2 Ko au lieu de lignes:

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

Réponse originale

Utilisez BoundedInputStream from Apache Commons IO library. Votre travail devient beaucoup plus facile.

Le code suivant fera ce que vous voulez:

public static String getContentFromInputStream(InputStream inputStream) {
  inputStream = new BoundedInputStream(inputStream, <number-of-bytes>);
  // Rest code are all same

Vous enveloppez simplement votre InputStream avec un BoundedInputStream et vous spécifiez une taille maximale. BoundedInputStream se chargera de limiter les lectures jusqu'à cette taille maximale.

Ou vous pouvez le faire lorsque vous créez le lecteur:

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

Fondamentalement, nous limitons la taille de lecture au niveau de la variable InputStream, plutôt que de le faire lors de la lecture de lignes. Vous vous retrouvez donc avec un composant réutilisable tel que BoundedInputStream qui limite la lecture au niveau de la couche InputStream et que vous pouvez utiliser où vous voulez.

Edit: Note de bas de page ajoutée

Edit 2: Ajouté la réponse mise à jour basée sur les commentaires

32
Subhas

Il existe essentiellement 4 façons de traiter un fichier:

  1. Traitement basé sur le flux (modèle Java.io.InputStream): placez éventuellement un bufferedReader autour du flux, effectuez une itération et lisez le prochain texte disponible du flux (si aucun texte n'est disponible, bloquer jusqu'à ce que certains deviennent disponibles), traiter chaque texte indépendamment lors de la lecture (pour des tailles de texte très variables)

  2. Traitement non bloquant basé sur des morceaux (modèle Java.nio.channels.Channel): créez un ensemble de tampons de taille fixe (représentant les "morceaux" à traiter), lus tour à tour dans chacun des tampons sans les bloquer (délégués d'API nio en natif, à l’aide de threads rapides de niveau O/S), votre thread de traitement principal sélectionne chaque tampon une fois rempli et traite le bloc de taille fixe, car les autres tampons continuent à être chargés de manière asynchrone.

  3. Traitement de fichier partiel (y compris le traitement ligne par ligne) (peut utiliser (1) ou (2) pour isoler ou construire chaque "partie"): divisez votre format de fichier en sous-parties ayant une signification sémantique (si Il est également possible de séparer les lignes!), de parcourir des morceaux de flux ou des morceaux et d’accumuler du contenu en mémoire jusqu’à ce que la partie suivante soit entièrement construite, traitez chaque partie dès qu’elle est construite.

  4. Traitement de fichier complet (modèle Java.nio.file.Files): lit le fichier entier en mémoire en une seule opération, traite le contenu complet

Lequel devriez-vous utiliser?
Cela dépend du contenu de votre fichier et du type de traitement requis.
Du point de vue de l'efficacité d'utilisation des ressources (le meilleur au pire) est: 1,2,3,4.
Du point de vue de la rapidité et de l’efficacité du traitement (du meilleur au pire) est la suivante: 2,1,3,4.
Du point de vue de la facilité de programmation (du meilleur au pire): 4,3,1,2.
Cependant, certains types de traitement peuvent nécessiter plus que le plus petit élément de texte (en excluant 1 et peut-être 2) et certains formats de fichiers peuvent ne pas comporter de parties internes (en excluant 3).

Vous faites 4. Je vous suggère de passer à 3 (ou moins), si vous pouvez.

Sous 4, il n'y a qu'un seul moyen d'éviter le DOS: limiter la taille avant sa lecture en mémoire (ou sa copie dans votre système de fichiers). Il est trop tard une fois lu. Si cela n’est pas possible, essayez 3, 2 ou 1. 

Limiter la taille du fichier

Le fichier est souvent téléchargé via un formulaire HTML. 

Si vous téléchargez à l'aide de l'annotation Servlet @MultipartConfig et de request.getPart().getInputStream(), vous avez le contrôle sur la quantité de données que vous lisez dans le flux. De plus, request.getPart().getSize() renvoie la taille du fichier à l’avance. Si elle est suffisamment petite, vous pouvez utiliser request.getPart().write(path) pour écrire le fichier sur le disque.

Si vous téléchargez à l'aide de JSF, JSF 2.2 (très nouveau) contient le composant HTML standard <h:inputFile> (javax.faces.component.html.InputFile), qui possède un attribut pour maxLength; Les implémentations antérieures à JSF 2.2 ont des composants personnalisés similaires (par exemple, Tomahawk a <t:InputFileUpload> avec l'attribut maxLength; PrimeFaces a <p:FileUpload> avec l'attribut sizeLimit.

Alternatives pour lire le fichier entier

Votre code, qui utilise InputStream, StringBuilder, etc., est un moyen efficace de lire l'intégralité du fichier, mais n'est pas nécessairement le moyen le plus simple (moindres lignes de code). 

Les développeurs débutants/moyens peuvent avoir la fausse impression que vous traitez efficacement le flux de données lorsque vous traitez l'intégralité du fichier - incluez donc les commentaires appropriés. 

Si vous voulez moins de code, vous pouvez essayer l'une des solutions suivantes:

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

 or 

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

Mais ils nécessitent des soins, ou ils pourraient être inefficaces dans l'utilisation des ressources. Si vous utilisez readAllLines puis concaténez les éléments List en une seule String, vous utiliserez alors le double de la mémoire (pour les éléments List + la String concaténée). De même, si vous utilisez readAllBytes, suivi d'un encodage en String (new String(byteContents, charset)), vous utilisez encore une fois "double" la mémoire. Il est donc préférable de traiter directement avec List<String> ou byte[], sauf si vous limitez vos fichiers à une taille suffisamment petite.

14
Glen Best

au lieu de readLine, utilisez read qui lit une quantité donnée de caractères.

dans chaque boucle, vérifiez la quantité de données lues, si elles dépassent une certaine quantité, plus que le maximum d'une entrée attendue, arrêtez-la, renvoyez une erreur et enregistrez-la.

3
Christian

Une note supplémentaire, j'ai remarqué que vous n'avez pas fermé votre BufferedInputStream. Vous devez fermer le bloc finally à BufferedReader, car il est sujet aux fuites de mémoire.

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

Pas besoin de fermer explicitement new InputStreamReader(inputStream) car cela sera automatiquement fermé lorsque vous appelez pour fermer la classe d'habillage bufferedReader

2
mel3kings

J'ai rencontré un problème similaire lors de la copie d'un énorme fichier binaire (qui ne contient généralement pas de caractère de nouvelle ligne). faire une readline () conduit à lire le fichier binaire entier dans une seule chaîne, causant OutOfMemory sur l'espace de tas.

Voici une alternative simple au 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();
}

Choses à noter:

  • L'exemple ci-dessus copie le fichier à l'aide d'un tampon de 1 Ko. Toutefois, si vous effectuez cette copie sur le réseau, vous souhaiterez peut-être modifier la taille de la mémoire tampon.

  • Si vous souhaitez utiliser FileChannel ou des bibliothèques telles que Commons IO , assurez-vous simplement que la mise en œuvre se résume à quelque chose comme ci-dessus.

2
Chris

Cela a fonctionné pour moi sans aucun problème.

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

Il existe une classe EntityUtils sous Apache httpCore. Utilisez la méthode getString () de cette classe pour obtenir la chaîne à partir du contenu de la réponse.

0
Sanjeev

Je ne peux pas penser à une solution autre que Apache Commons IO FileUtils. C'est assez simple avec la classe FileUtils, car la prétendue attaque DOS ne viendra pas directement de la couche supérieure . Lire et écrire un fichier est très simple, vous pouvez le faire avec une seule ligne de code

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

Vous pouvez explorer plus à ce sujet.

0
Kris