Salto de directorio del archivo ZIP

Categoría de OWASP: MASVS-STORAGE: Almacenamiento

Descripción general

La vulnerabilidad de salto de directorio del archivo ZIP, también conocida como ZipSlip, está relacionada con el manejo de archivos comprimidos. En esta página, demostramos esta vulnerabilidad a través del uso del formato ZIP como ejemplo, pero pueden surgir problemas similares en bibliotecas que manejan otros formatos, como TAR, RAR o 7z.

El motivo subyacente de este problema es que, dentro de los archivos ZIP, cada archivo empaquetado se almacena con un nombre completamente calificado, lo que permite caracteres especiales, como barras y puntos. La biblioteca predeterminada del paquete java.util.zip no verifica los nombres de las entradas de archivo para los caracteres de recorrido del directorio (../), por lo que se debe tener especial cuidado cuando se concatena el nombre extraído del archivo con la ruta del directorio de destino.

Es muy importante validar cualquier fragmento de código de extracción de archivos ZIP o bibliotecas de fuentes externas. Muchas de esas bibliotecas son vulnerables a los saltos de directorio del archivo ZIP.

Impacto

La vulnerabilidad de salto de directorio del archivo ZIP se puede usar para reemplazar archivos de forma arbitraria. Según las condiciones, el impacto puede variar, pero en muchos casos, esta vulnerabilidad puede causar problemas de seguridad importantes, como la ejecución del código.

Mitigaciones

Para mitigar este problema, antes de extraer cada entrada, siempre debes verificar que la ruta de destino sea un elemento secundario del directorio de destino. En el siguiente código, se da por sentado que el directorio de destino es seguro (solo tu app puede escribir en él y los atacantes no lo controlan); de lo contrario, tu app podría ser propensa a otras vulnerabilidades, como los ataques de symlink.

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

Para evitar reemplazar archivos existentes por accidente, también debes asegurarte de que el directorio de destino esté vacío antes de comenzar el proceso de extracción. De lo contrario, te arriesgas a que falle la app o, en casos extremos, a que se vea comprometida.

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

Recursos

  • Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
  • Salto de directorio