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
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Salto de directorio