Zip Path Traversal

Categoria OWASP: MASVS-STORAGE: Storage

Panoramica

La vulnerabilità Zip Path Traversal, nota anche come ZipSlip, è correlata alla gestione degli archivi compressi. In questa pagina, dimostriamo questa vulnerabilità utilizzando il formato ZIP come esempio, ma problemi simili possono verificarsi nelle librerie che gestiscono altri formati, come TAR, RAR o 7z.

Il motivo di fondo di questo problema è che all'interno degli archivi ZIP ogni file compresso viene archiviato con un nome completo, che consente caratteri speciali come barre e punti. La libreria predefinita del pacchetto java.util.zip non controlla i nomi delle voci dell'archivio per i caratteri di attraversamento della directory (../), pertanto è necessario prestare particolare attenzione quando si concatena il nome estratto dall'archivio con il percorso della directory di destinazione.

È molto importante convalidare eventuali snippet di codice o librerie di estrazione ZIP da fonti esterne. Molte di queste librerie sono vulnerabili agli attraversamenti di percorso Zip.

Impatto

La vulnerabilità Zip Path Traversal può essere utilizzata per ottenere la sovrascrittura arbitraria dei file. A seconda delle condizioni, l'impatto può variare, ma in molti casi questa vulnerabilità può portare a gravi problemi di sicurezza, come l'esecuzione di codice.

Mitigazioni

Per risolvere questo problema, prima di estrarre ogni voce, devi sempre verificare che il percorso di destinazione sia un elemento secondario della directory di destinazione. Il codice riportato di seguito presuppone che la directory di destinazione sia sicura, ovvero scrivibile solo dalla tua app e non sotto il controllo di un malintenzionato. In caso contrario, la tua app potrebbe essere soggetta ad altre vulnerabilità, come gli attacchi tramite link simbolici.

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

Per evitare di sovrascrivere accidentalmente i file esistenti, assicurati anche che la directory di destinazione sia vuota prima di iniziare il processo di estrazione. In caso contrario, rischi potenziali arresti anomali dell'app o, in casi estremi, una compromissione dell'applicazione.

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

Risorse