Skip to main content

Zip Path Traversal

Zip Path Traversal (or ZipSlip) is a security vulnerability related to compressed archives handling.

Taken from Android Developers website:

The underlying reason for this problem is that inside ZIP archives, each packed file is stored with a fully qualified name, which allows special characters such as slashes and dots. The default library from the java.util.zip package doesn't check the names of the archive entries for directory traversal characters (../), so special care must be taken when concatenating the name extracted from the archive with the targeted directory path.

Here's an excellent video from Snyk demonstrating the vulnerability in action:

Vulnerability Demonstration​

Let's say we have we a simple utility class to extract .zip files like this:

ZipUtility.kt
package com.hanmajid.androidnotebook

import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

/**
* Zip file-related utility object.
*/
object ZipUtility {

/**
* Unzip file from [inputStream] into [destinationDir].
*/
@JvmStatic
fun unzip(inputStream: InputStream, destinationDir: File) {
// Check whether destination is a directory.
if (!destinationDir.isDirectory) {
throw IOException("Destination is not a directory.")
}
// Check whether destination is empty.
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)
if (targetFile.isDirectory) {
targetFile.mkdirs()
} else {
val parentDir = targetFile.parentFile
if (parentDir != null && !parentDir.isDirectory) {
parentDir.mkdirs()
}
BufferedOutputStream(FileOutputStream(targetFile)).use {
zipInputStream.copyTo(it)
}
}
}
}
}
}

Then we need to prepare a .zip file that contains directory traversal characters (../). We can use the excellent cesarsotovalero/zip-slip-exploit-example repository to create our malicious .zip file.

For this example, I have created a evil.zip file that contains this file: ../../evil.sh and put it in my raw resource folder.

Lastly we just need run the ZipUtility.unzip() method to see the vulnerability in action:

import android.content.Context
import java.io.File

val destinationDir = File(context.filesDir, "real_folder")
destinationDir.mkdirs()
context.resources.openRawResource(R.raw.evil).use {
ZipUtility.unzip(it, destinationDir)
}

If you look inside your device's internal storage, you would notice that the extracted file is outside of its intended folder (real_folder):

This is just a simple demonstration, but it could lead to other major security issues such as code execution.

Mitigation​

For Android 14 and above​

If your app is targeting Android 14 (API level 34), the system automatically prevents Zip Path Traversal by throwing ZipException if the zip file entry names contain .. or start with / like this:

For Android 13 and below​

If your app is targeting Android 13 and below, you can mitigate Zip Path Traversal by creating a method that checks the extracted file yourself:

import java.io.File
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipException

/**
* Zip file-related utility object.
*/
object ZipUtility {

// ...

/**
* Checks whether the extracted [zipEntry] is safe from path traversal.
*/
@JvmStatic
@Throws(IOException::class)
fun newFile(destinationDir: File, zipEntry: ZipEntry): File {
val name: String = zipEntry.name
val f = File(destinationDir, name)
val canonicalPath = f.canonicalPath
if (!canonicalPath.startsWith(
destinationDir.canonicalPath + File.separator
)
) {
throw ZipException("Illegal name: $name")
}
return f
}
}

Then you use this method inside the unzip() method:

// Replace this line:
// val targetFile = File(destinationDir, zipEntry!!.name)
// With this line:
val targetFile = newFile(destinationDir, zipEntry!!)

References​