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.
También te puede interesar
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
/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 todo | ZipFile + entries() |
| Leer una entrada concreta | zf.getEntry(nombre) |
| Un ZIP que llega por la red | ZipInputStream |
| Trabajar dentro como si fuera una carpeta | FileSystems.newFileSystem |
| No dejarte hacer un Zip Slip | normalize() 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.