Skip to content

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

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

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