ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing
ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing
Description
ZIP files, which are compressed archives used to store and transmit multiple files, are a widely adopted file format due to their convenience and compatibility across different platforms. However, like any digital file format, ZIP files are not immune to vulnerabilities. Here are some common vulnerabilities associated with ZIP files:
Path Traversal
Path traversal, also known as directory traversal or directory climbing, is a vulnerability that allows an attacker to access files or directories outside of the intended extraction directory. When extracting a ZIP file, if the extraction process does not properly validate the file paths within the archive, an attacker can craft a malicious ZIP file containing special characters or sequences that enable them to traverse directories and access sensitive files on the system. This can lead to unauthorized disclosure of sensitive information or even remote code execution.
import 'dart:io';
import 'archive/archive.dart';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
// Insecure: Does not properly validate file paths
File extractedFile = File('/tmp/' + archiveFile.name);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
}
}
import Foundation
import ZIPFoundation
func extractZipFile(path: String) {
guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
return
}
for entry in archive {
// Insecure: Does not properly validate file paths
let extractedFilePath = "/tmp/\(entry.path)"
let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
do {
try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
withIntermediateDirectories: true,
attributes: nil)
try archive.extract(entry, to: extractedFileURL)
} catch {
print("Extraction failed: \(error.localizedDescription)")
}
}
}
import java.io.File
import java.util.zip.ZipInputStream
fun extractZipFile(path: String) {
val file = File(path)
val zipInputStream = ZipInputStream(file.inputStream())
var entry = zipInputStream.nextEntry
while (entry != null) {
// Insecure: Does not properly validate file paths
val extractedFile = File("/tmp/" + entry.name)
extractedFile.parentFile.mkdirs()
extractedFile.outputStream().use { output ->
zipInputStream.copyTo(output)
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
}
Zip Symbolic Link
Symbolic links, or symlinks, are pointers to files or directories that can be used to create shortcuts or references. However, if a ZIP file extraction process does not handle symbolic links properly, an attacker can craft a malicious ZIP file that includes symbolic links pointing to sensitive files or directories on the target system. Upon extraction, these symbolic links can be followed, leading to unauthorized access to critical files or directories.
import 'dart:io';
import 'archive/archive.dart';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
// Insecure: Does not handle symbolic links properly
if (archiveFile.isSymbolicLink) {
File symlink = File('/tmp/' + archiveFile.name);
symlink.createSync(recursive: true);
symlink.writeAsStringSync(archiveFile.content);
} else {
File extractedFile = File('/tmp/' + archiveFile.name);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
}
}
}
import Foundation
import ZIPFoundation
func extractZipFile(path: String) {
guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
return
}
for entry in archive {
// Insecure: Does not handle symbolic links properly
let extractedFilePath = "/tmp/\(entry.path)"
let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
if entry.type == .symbolicLink {
do {
try FileManager.default.createSymbolicLink(at: extractedFileURL, withDestinationURL: entry.destinationURL)
} catch {
print("Symbolic link creation failed: \(error.localizedDescription)")
}
} else {
do {
try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
withIntermediateDirectories: true,
attributes: nil)
try archive.extract(entry, to: extractedFileURL)
} catch {
print("Extraction failed: \(error.localizedDescription)")
}
}
}
}
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream
fun extractZipFile(path: String) {
val file = File(path)
val zipInputStream = ZipInputStream(file.inputStream())
var entry = zipInputStream.nextEntry
while (entry != null) {
// Insecure: Does not handle symbolic links properly
val extractedFile = File("/tmp/" + entry.name)
if (entry.isSymbolicLink) {
Files.createSymbolicLink(Path.of(extractedFile.path), Path.of(entry.link))
} else {
extractedFile.parentFile.mkdirs()
extractedFile.outputStream().use { output ->
zipInputStream.copyTo(output)
}
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
}
Zip Extension Spoofing
Zip extension spoofing is a technique where an attacker spoofs the extension of a malicious file within a ZIP file to deceive users and security systems. By manipulating the ZIP file headers, the attacker can change the extension of the file within the archive to seem harmless. However, when the user extracts the file or opens it with a vulnerable application, the malicious payload is executed, potentially leading to unauthorized code execution, malware infection, or other malicious activities.
import 'dart:io';
import 'archive/archive.dart';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
// Insecure: the zip decoder parses the filename from the Local File Header which can be manipulated
// Zipdecoder has to check against the central directory to make sure the extension was not altered
String extractedFilePath = '/tmp/' + archiveFile.name;
if (archiveFile.name.endsWith('.zip')) {
// Extracting a ZIP file within a ZIP file
extractZipFile(extractedFilePath);
} else {
File extractedFile = File(extractedFilePath);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
}
}
}
import java.io.File
import java.util.zip.ZipInputStream
fun extractZipFile(path: String) {
val file = File(path)
val zipInputStream = ZipInputStream(file.inputStream())
var entry = zipInputStream.nextEntry
while (entry != null) {
// Insecure: the zip decoder parses the filename from the Local File Header which can be altered
// Zipdecoder has to check against the central directory to make sure the extension was not altered
val extractedFile = File("/tmp/" + entry.name)
if (entry.name.endsWith(".zip")) {
// Extracting a ZIP file within a ZIP file
extractZipFile(extractedFile.path)
} else {
extractedFile.parentFile.mkdirs()
extractedFile.outputStream().use { output ->
zipInputStream.copyTo(output)
}
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
}
Recommendation
To mitigate the risks associated with zip files, consider the following:
- For each zip entry to extract, standardize the path using a standard library and check if it's contained within the extraction directory.
- Implement proper input validation and sanitization to prevent user-supplied input from containing directory traversal sequences.
import 'dart:io';
import 'archive/archive.dart';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
String extractedFilePath = '/tmp/' + sanitizeFilePath(archiveFile.name);
// Check if the extracted file path is within the allowed directory
if (isPathWithinAllowedDirectory(extractedFilePath)) {
File extractedFile = File(extractedFilePath);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
}
}
}
String sanitizeFilePath(String filePath) {
// Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
// Return the sanitized file path
}
bool isPathWithinAllowedDirectory(String filePath) {
// Implement logic to check if the extracted file path is within the allowed directory
// Return true if the file path is allowed, false otherwise
}
import Foundation
func extractZipFile(path: String) {
guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
return
}
let destinationDir = URL(fileURLWithPath: "/tmp/")
for entry in archive {
let extractedFilePath = sanitizeFilePath(entry.path)
// Check if the extracted file path is within the allowed directory
if isPathWithinAllowedDirectory(extractedFilePath) {
let extractedFileURL = destinationDir.appendingPathComponent(extractedFilePath)
do {
try archive.extract(entry, to: extractedFileURL)
} catch {
print("Error extracting file: \(error)")
}
}
}
}
func sanitizeFilePath(_ filePath: String) -> String {
// Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
// Return the sanitized file path
}
func isPathWithinAllowedDirectory(_ filePath: String) -> Bool {
// Implement logic to check if the extracted file path is within the allowed directory
// Return true if the file path is allowed, false otherwise
}
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
fun extractZipFile(path: String) {
val file = File(path)
val zipFile = ZipFile(file)
val destinationDir = File("/tmp/")
val zipEntries = zipFile.entries()
while (zipEntries.hasMoreElements()) {
val zipEntry = zipEntries.nextElement()
val extractedFilePath = sanitizeFilePath(zipEntry.name)
// Check if the extracted file path is within the allowed directory
if (isPathWithinAllowedDirectory(extractedFilePath)) {
val extractedFile = File(destinationDir, extractedFilePath)
extractedFile.parentFile.mkdirs()
extractedFile.outputStream().use { outputStream ->
zipFile.getInputStream(zipEntry).copyTo(outputStream)
}
}
}
zipFile.close()
}
fun sanitizeFilePath(filePath: String): String {
// Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
// Return the sanitized file path
}
fun isPathWithinAllowedDirectory(filePath: String): Boolean {
// Implement logic to check if the extracted file path is within the allowed directory
// Return true if the file path is allowed, false otherwise
}
Zip Symbolic Link
- Before extracting files, check for symbolic links within the ZIP archive and ensure they are not followed blindly during extraction.
- Validate and sanitize the symbolic link target to prevent directory traversal or access to sensitive system files.
- Use platform-specific functions or libraries that handle symbolic links securely and prevent the creation of malicious links.
- Limit the extraction process to known-safe locations and avoid allowing symbolic links to be created outside of those boundaries.
- Ignore symlinks
import 'dart:io';
import 'archive/archive.dart';
import 'path';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
if (!isSymbolicLink(archiveFile)) {
// Extract regular file
String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
File extractedFile = File(extractedFilePath);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
}
}
}
bool isSymbolicLink(ArchiveFile archiveFile) {
// Implement platform-specific logic to check if the file is a symbolic link
// Return true if it is a symbolic link, false otherwise
}
import Foundation
import ZIPFoundation
func extractZipFile(path: String) {
let fileManager = FileManager.default
guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
return
}
for entry in archive {
if !isSymbolicLink(entry) {
// Extract regular file
let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
}
}
}
func isSymbolicLink(_ entry: Entry) -> Bool {
// Implement platform-specific logic to check if the entry is a symbolic link
// Return true if it is a symbolic link, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream
fun extractZipFile(path: String) {
val file = File(path)
val zipInput = ZipInputStream(file.inputStream())
var entry = zipInput.nextEntry
while (entry != null) {
if (!isSymbolicLink(entry)) {
// Extract regular file
val extractedFilePath = File("/tmp", sanitizePath(entry.name))
extractedFilePath.parentFile.mkdirs()
Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
entry = zipInput.nextEntry
}
}
fun isSymbolicLink(entry: ZipEntry): Boolean {
// Implement platform-specific logic to check if the entry is a symbolic link
// Return true if it is a symbolic link, false otherwise
}
Zip Extension Spoofing
- Perform additional checks or validations on the extracted files to ensure that their true file type matches the expected extension.
- Consider using file signatures or magic numbers to verify the file's content and compare it with the indicated extension.
- Implement file type verification based on both the extension and the file header to ensure consistency.
- Consider using third-party libraries or tools specifically designed to handle ZIP files securely, as they may provide built-in protection against extension spoofing attacks.
import 'dart:io';
import 'archive/archive.dart';
import 'path';
void extractZipFile(String path) {
File file = File(path);
Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (ArchiveFile archiveFile in archive) {
// Mitigation: Validate the file type by comparing the extension and file header
if (isFileExtensionValid(archiveFile) && isFileHeaderValid(archiveFile)) {
String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
File extractedFile = File(extractedFilePath);
extractedFile.createSync(recursive: true);
extractedFile.writeAsBytesSync(archiveFile.content);
} else {
// Handle case when file type does not match the expected extension
print('Invalid file type detected: ${archiveFile.name}');
}
}
}
bool isFileExtensionValid(ArchiveFile archiveFile) {
// Implement logic to validate the file extension against expected extensions
// Return true if the file extension is valid, false otherwise
}
bool isFileHeaderValid(ArchiveFile archiveFile) {
// Implement logic to validate the file header and ensure it matches the expected file type
// Return true if the file header is valid, false otherwise
}
import Foundation
import ZIPFoundation
func extractZipFile(path: String) {
let fileManager = FileManager.default
guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
return
}
for entry in archive {
// Mitigation: Validate the file type by comparing the extension and file header
if isFileExtensionValid(entry) && isFileHeaderValid(entry) {
let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
} else {
// Handle case when file type does not match the expected extension
print("Invalid file type detected: \(entry.path)")
}
}
}
func isFileExtensionValid(_ entry: Entry) -> Bool {
// Implement logic to validate the file extension against expected extensions
// Return true if the file extension is valid, false otherwise
}
func isFileHeaderValid(_ entry: Entry) -> Bool {
// Implement logic to validate the file header and ensure it matches the expected file type
// Return true if the file header is valid, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream
fun extractZipFile(path: String) {
val file = File(path)
val zipInput = ZipInputStream(file.inputStream())
var entry = zipInput.nextEntry
while (entry != null) {
// Mitigation: Validate the file type by comparing the extension and file header
if (isFileExtensionValid(entry) && isFileHeaderValid(entry)) {
val extractedFilePath = File("/tmp", sanitizePath(entry.name))
extractedFilePath.parentFile.mkdirs()
Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
} else {
// Handle case when file type does not match the expected extension
println("Invalid file type detected: ${entry.name}")
}
entry = zipInput.nextEntry
}
}
fun isFileExtensionValid(entry: ZipEntry): Boolean {
// Implement logic to validate the file extension against expected extensions
// Return true if the file extension is valid, false otherwise
}
fun isFileHeaderValid(entry: ZipEntry): Boolean {
// Implement logic to validate the file header and ensure it matches the expected file type
// Return true if the file header is valid, false otherwise
}
Links
Standards
- OWASP_MASVS_L1:
- MSTG_PLATFORM_2
- OWASP_MASVS_L2:
- MSTG_PLATFORM_2
- PCI_STANDARDS:
- REQ_2_2
- REQ_6_2
- REQ_6_3
- REQ_11_3
- OWASP_MASVS_v2_1:
- MASVS_CODE_4
- SOC2_CONTROLS:
- CC_2_1
- CC_4_1
- CC_7_1
- CC_7_2
- CC_7_4
- CC_7_5