@@ -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 }
0 commit comments