Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions Platform/UTMDownloadVMTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,64 @@ class UTMDownloadVMTask: UTMDownloadTask {
let destinationURL = destinationFolder.appendingPathComponent(destinationUtmDirectory, isDirectory: true)
/// create the .utm directory
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: false)
/// get and extract all files contained in the UTM directory, except the `__MACOSX` folder
let containedFiles = archive.filter({ $0.path.contains(utmDirectoryEnding) && !$0.path.hasSuffix(utmDirectoryEnding) && !$0.path.contains("__MACOSX") })
for file in containedFiles {
let relativePath = file.path.replacingOccurrences(of: utmFolderInZip.path, with: "")
let isDirectory = file.path.hasSuffix("/")
_ = try archive.extract(file, to: destinationURL.appendingPathComponent(relativePath, isDirectory: isDirectory), skipCRC32: true)
do {
/// get and extract all files contained in the UTM directory, except the `__MACOSX` folder
let containedFiles = archive.filter({ $0.path.hasPrefix(utmFolderInZip.path) && !$0.path.hasSuffix(utmDirectoryEnding) && !$0.path.contains("__MACOSX") })
for file in containedFiles {
/// we never store a symlink in a package, and a link target that survives a containment
/// check can still be resolved outside of the package when it is written through later
guard file.type != .symlink else {
throw UnzipUnsafePathError()
}
let relativePath = String(file.path.dropFirst(utmFolderInZip.path.count))
let isDirectory = file.path.hasSuffix("/")
let fileURL = try containedDestination(for: relativePath, in: destinationURL, isDirectory: isDirectory)
_ = try archive.extract(file, to: fileURL, skipCRC32: true)
}
} catch {
/// a partially extracted package would still be picked up as a VM by the library
try? fileManager.removeItem(at: destinationURL)
throw error
}
return destinationURL
} else {
throw UnzipNoUTMFileError()
}
}

private class UnzipNoUTMFileError: Error {
/// Resolve an archive entry's relative path inside `destinationFolder` and reject any escape.
///
/// A crafted archive can name an entry `some.utm/../../elsewhere`. `appendingPathComponent()` does not
/// resolve `..`, so the traversal would only be resolved by the filesystem at write time.
private func containedDestination(for relativePath: String, in destinationFolder: URL, isDirectory: Bool) throws -> URL {
let candidate = destinationFolder.appendingPathComponent(relativePath, isDirectory: isDirectory)
/// POSIX `fopen()` collapses repeated separators before resolving `..`, so an entry named `/../elsewhere`
/// would otherwise standardize to a contained path here but escape once written. Collapse them first.
var path = candidate.path
while path.contains("//") {
path = path.replacingOccurrences(of: "//", with: "/")
}
let resolved = URL(fileURLWithPath: path, isDirectory: isDirectory).standardized
let root = URL(fileURLWithPath: destinationFolder.path, isDirectory: true).standardized
guard resolved.path.hasPrefix(root.path + "/") else {
throw UnzipUnsafePathError()
}
return resolved
}

private class UnzipUnsafePathError: LocalizedError {
var errorDescription: String? {
NSLocalizedString("The downloaded ZIP archive contains an invalid path.", comment: "Error shown when importing a ZIP file from web that contains an entry pointing outside of the virtual machine directory.")
}
}

private class UnzipNoUTMFileError: LocalizedError {
var errorDescription: String? {
NSLocalizedString("There is no UTM file in the downloaded ZIP archive.", comment: "Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine.")
}
}

private class CreateUTMFailed: Error {
private class CreateUTMFailed: LocalizedError {
var errorDescription: String? {
NSLocalizedString("Failed to parse the downloaded VM.", comment: "UTMDownloadVMTask")
}
Expand Down
4 changes: 2 additions & 2 deletions UTM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -5509,7 +5509,7 @@
repositoryURL = "https://github.com/weichsel/ZIPFoundation.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.17;
minimumVersion = 0.9.20;
};
};
84018693288B66370050AC51 /* XCRemoteSwiftPackageReference "swiftui-visual-effects" */ = {
Expand Down Expand Up @@ -5653,7 +5653,7 @@
repositoryURL = "https://github.com/weichsel/ZIPFoundation.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.17;
minimumVersion = 0.9.20;
};
};
CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading