Skip to content

Commit a6e9b04

Browse files
committed
Validate symlink targets during ZIP extraction to prevent path traversal
1 parent 3a7fb59 commit a6e9b04

File tree

2 files changed

+26
-3
lines changed

2 files changed

+26
-3
lines changed

Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Reading.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ extension Archive {
2626
to url: URL,
2727
bufferSize: Int = defaultReadChunkSize,
2828
skipCRC32: Bool = false,
29-
progress: Progress? = nil
29+
progress: Progress? = nil,
30+
allowedDestination: URL? = nil
3031
) throws -> CRC32 {
3132
guard bufferSize > 0 else {
3233
throw ArchiveError.invalidBufferSize
@@ -71,6 +72,22 @@ extension Archive {
7172
}
7273
let consumer = { (data: Data) in
7374
guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath }
75+
// Validate that the symlink target resolves within the allowed destination directory.
76+
// Without this check, a malicious ZIP could create symlinks pointing to arbitrary system files.
77+
if let allowedDestination = allowedDestination {
78+
let resolvedTarget: URL
79+
if linkPath.hasPrefix("/") {
80+
resolvedTarget = URL(fileURLWithPath: linkPath).standardized
81+
} else {
82+
resolvedTarget = url.deletingLastPathComponent().appendingPathComponent(linkPath).standardized
83+
}
84+
guard resolvedTarget.isContained(in: allowedDestination) else {
85+
throw CocoaError(
86+
.fileReadInvalidFileName,
87+
userInfo: [NSFilePathErrorKey: resolvedTarget.path]
88+
)
89+
}
90+
}
7491
try fileManager.createParentDirectoryStructure(for: url)
7592
try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath)
7693
}

Sources/Private/EmbeddedLibraries/ZipFoundation/FileManager+ZIP.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,15 @@ extension FileManager {
251251
if let progress {
252252
let entryProgress = archive.makeProgressForReading(entry)
253253
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
254-
crc32 = try archive.extract(entry, to: entryURL, skipCRC32: skipCRC32, progress: entryProgress)
254+
crc32 = try archive.extract(
255+
entry,
256+
to: entryURL,
257+
skipCRC32: skipCRC32,
258+
progress: entryProgress,
259+
allowedDestination: destinationURL
260+
)
255261
} else {
256-
crc32 = try archive.extract(entry, to: entryURL, skipCRC32: skipCRC32)
262+
crc32 = try archive.extract(entry, to: entryURL, skipCRC32: skipCRC32, allowedDestination: destinationURL)
257263
}
258264

259265
func verifyChecksumIfNecessary() throws {

0 commit comments

Comments
 (0)