Java – Leer archivo ZIP

Extraer un ZIP son veinte líneas, y tres de ellas son de seguridad. Sin esas tres, un ZIP preparado a mala idea puede escribir archivos donde le dé la gana: es la vulnerabilidad conocida como Zip Slip.

Extraer, con la comprobación puesta

import java.io.*;
import java.nio.file.*;
import java.util.zip.*;

public class LeerZip {
    public static void main(String[] args) throws Exception {
        Path zip = Path.of("copia.zip");
        Path destino = Path.of("extraido").toAbsolutePath().normalize();

        try (var zf = new ZipFile(zip.toFile())) {
            var entradas = zf.entries();
            while (entradas.hasMoreElements()) {
                ZipEntry entrada = entradas.nextElement();
                Path salida = destino.resolve(entrada.getName()).normalize();

                if (!salida.startsWith(destino)) {
                    throw new IOException("entrada peligrosa: " + entrada.getName());
                }
                if (entrada.isDirectory()) {
                    Files.createDirectories(salida);
                    continue;
                }
                Files.createDirectories(salida.getParent());
                try (var in = zf.getInputStream(entrada)) {
                    Files.copy(in, salida, StandardCopyOption.REPLACE_EXISTING);
                }
                System.out.println("  extraído " + destino.relativize(salida)
                                 + " (" + Files.size(salida) + " bytes)");
            }
        }
    }
}
salidaextraído a.txt (8 bytes)
  extraído b.txt (8 bytes)
  extraído sub/c.txt (7 bytes)

Por qué esas tres líneas

El nombre de una entrada de un ZIP es texto libre, y nada impide que contenga ... Esto es lo que pasa con una entrada preparada:

try (var z = new ZipOutputStream(Files.newOutputStream(Path.of("malo.zip")))) {
    z.putNextEntry(new ZipEntry("../../fuera.txt"));
    z.write("te he escrito fuera de la carpeta".getBytes());
    z.closeEntry();
}

Path destino = Path.of("extraido").toAbsolutePath().normalize();
Path salida = destino.resolve(entrada.getName()).normalize();

System.out.println("la entrada se llama : " + entrada.getName());
System.out.println("y acabaría en       : " + salida);
System.out.println("¿está dentro?       : " + salida.startsWith(destino));
salidala entrada se llama : ../../fuera.txt
y acabaría en       : /trabajo/fuera.txt
¿está dentro?       : false
Eso es Zip Slip. El destino era /trabajo/l3z/extraido y el archivo habría acabado en /trabajo, dos niveles más arriba. Con las rutas adecuadas se sobrescribe un archivo de configuración, una clave SSH o un .jsp dentro de un servidor. La defensa cabe en dos líneas: resolver la ruta, normalizarla y comprobar que sigue empezando por el destino. El normalize() es imprescindible, porque es el que resuelve los ..; sin él, la comprobación pasa y el archivo se escribe fuera igual.

La misma precaución vale para los tar, para los adjuntos de un correo y para cualquier formato donde el nombre del archivo lo elija quien envía.

Leer una sola entrada

try (var zf = new ZipFile("copia.zip")) {
    ZipEntry e = zf.getEntry("sub/c.txt");
    if (e != null) {
        try (var in = zf.getInputStream(e)) {
            System.out.println(new String(in.readAllBytes(), StandardCharsets.UTF_8));
        }
    }
}

ZipFile lee la tabla de contenidos del final y puede saltar directamente a una entrada, sin descomprimir las demás. Si lo que tienes es un flujo y no un archivo —algo que llega por la red— usa ZipInputStream, que va secuencialmente y no permite saltar.

El ZIP como sistema de archivos

try (FileSystem fs = FileSystems.newFileSystem(Path.of("copia.zip"))) {
    Path dentro = fs.getPath("sub", "c.txt");
    System.out.println(Files.readString(dentro));

    Files.writeString(fs.getPath("nuevo.txt"), "añadido sin descomprimir");
}

Esto es lo que casi nadie sabe: desde Java 7 un ZIP se puede montar como un FileSystem y entonces valen Files.readString, Files.walk y todo lo demás, incluido escribir dentro sin descomprimir y volver a comprimir. Para modificar un archivo dentro de un ZIP es, con diferencia, lo más cómodo.

Para…Usa
Extraerlo todoZipFile + entries()
Leer una entrada concretazf.getEntry(nombre)
Un ZIP que llega por la redZipInputStream
Trabajar dentro como si fuera una carpetaFileSystems.newFileSystem
No dejarte hacer un Zip Slipnormalize() y startsWith(destino)

Más información en inglés: Extract files from a ZIP in Java.

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.

Deja un comentario