Traversée de répertoires ZIP

Catégorie OWASP : MASVS-STORAGE : stockage

Présentation

La faille de traversée de répertoires ZIP, également appelée ZipSlip, est liée à la gestion des archives compressées. Sur cette page, nous présentons cette faille en prenant le format ZIP comme exemple, mais des problèmes similaires peuvent survenir dans les bibliothèques gérant d'autres formats, tels que TAR, RAR ou 7z.

La raison sous-jacente de ce problème est que, dans les archives ZIP, chaque fichier empaqueté est stocké sous un nom complet, ce qui permet d'utiliser des caractères spéciaux tels que des barres obliques et des points. La bibliothèque par défaut du package java.util.zip ne vérifie pas la présence de caractères de traversée de répertoire dans les noms des entrées d'archive (../). Vous devez donc faire attention lorsque vous concaténez le nom extrait de l'archive avec le chemin de répertoire ciblé.

Il est très important de valider les extraits de code ZIP ou les bibliothèques provenant de sources externes. Bon nombre de ces bibliothèques sont vulnérables aux traversées de répertoires ZIP.

Impact

La faille de traversée de répertoires ZIP permet d'écraser des fichiers de façon arbitraire. L'impact peut varier en fonction des conditions, mais dans de nombreux cas cette faille peut entraîner des problèmes de sécurité majeurs tels que l'exécution du code.

Stratégies d'atténuation

Pour atténuer ce problème, avant d'extraire chaque entrée, vous devez toujours vérifier que le chemin d'accès cible est un enfant du répertoire de destination. Le code ci-dessous part du principe que le répertoire de destination est sûr (accessible en écriture que par votre application et n'est pas contrôlé par des pirates informatiques), autrement votre application pourrait être exposée à d'autres failles telles que des attaques par liens symboliques.

Kotlin

companion object {
    @Throws(IOException::class)
    fun newFile(targetPath: File, zipEntry: ZipEntry): File {
        val name: String = zipEntry.name
        val f = File(targetPath, name)
        val canonicalPath = f.canonicalPath
        if (!canonicalPath.startsWith(
                targetPath.canonicalPath + File.separator)) {
            throw ZipException("Illegal name: $name")
        }
        return f
    }
}

Java

public static File newFile(File targetPath, ZipEntry zipEntry) throws IOException {
    String name = zipEntry.getName();
    File f = new File(targetPath, name);
    String canonicalPath = f.getCanonicalPath();
    if (!canonicalPath.startsWith(targetPath.getCanonicalPath() + File.separator)) {
      throw new ZipException("Illegal name: " + name);
    }
    return f;
 }

Pour éviter d'écraser accidentellement des fichiers existants, assurez-vous que le répertoire de destination est vide avant de commencer le processus d'extraction. Sinon, vous risquez de faire planter les applications ou, dans des cas extrêmes, de compromettre leur sécurité.

Kotlin

@Throws(IOException::class)
fun unzip(inputStream: InputStream?, destinationDir: File) {
    if (!destinationDir.isDirectory) {
        throw IOException("Destination is not a directory.")
    }
    val files = destinationDir.list()
    if (files != null && files.isNotEmpty()) {
        throw IOException("Destination directory is not empty.")
    }
    ZipInputStream(inputStream).use { zipInputStream ->
        var zipEntry: ZipEntry
        while (zipInputStream.nextEntry.also { zipEntry = it } != null) {
            val targetFile = File(destinationDir, zipEntry.name)
            // ...
        }
    }
}

Java

void unzip(final InputStream inputStream, File destinationDir)
      throws IOException {
  if(!destinationDir.isDirectory()) {
    throw IOException("Destination is not a directory.");
  }

  String[] files = destinationDir.list();
  if(files != null && files.length != 0) {
    throw IOException("Destination directory is not empty.");
  }

  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
    ZipEntry zipEntry;
    while ((zipEntry = zipInputStream.getNextEntry()) != null) {
      final File targetFile = new File(destinationDir, zipEntry);
        …
    }
  }
}

Ressources