From 381dea01c11a94e8f3e0801ad5f9ea5f7d0dddfb Mon Sep 17 00:00:00 2001 From: J Date: Sat, 17 Oct 2026 03:27:08 +0530 Subject: [PATCH 1/2] Reject ZIP entry names treated as unzip switches or glob selectors --- Sources/App/ZIP/ZipInspector.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/App/ZIP/ZipInspector.swift b/Sources/App/ZIP/ZipInspector.swift index da71b65..b5919ef 100644 --- a/Sources/App/ZIP/ZipInspector.swift +++ b/Sources/App/ZIP/ZipInspector.swift @@ -134,7 +134,11 @@ public enum ZipInspector { } private static func validatePath(_ name: String) throws { - guard !name.hasPrefix("/"), !name.hasPrefix("\\"), + guard !name.hasPrefix("/"), !name.hasPrefix("\\"), !name.hasPrefix("-"), + // unzip treats selected entries as glob patterns, not literal + // strings. Reject patterns rather than expanding selections. + !name.contains("*"), !name.contains("?"), + !name.contains("["), !name.contains("]"), !name.contains("\\"), !name.contains("\0"), !name.contains(":"), !name.contains("//"), name.utf8.count <= 1024, !name.split(separator: "/").contains(where: { $0 == ".." || $0 == "." }), @@ -150,7 +154,7 @@ public enum ZipInspector { selected: Set?, shouldCancel: @escaping @Sendable () -> Bool = { false }) throws -> URL { let listing = try inspect(archive) - let picked = listing.filter { selected == nil || selected!.contains($0.name) } + let picked = listing.filter { selected?.contains($0.name) ?? true } guard !picked.isEmpty else { throw Invalid.malformed("No ZIP entries selected.") } guard !shouldCancel() else { throw CancellationError() } let fm = FileManager.default From 5532a6e2dd2e220a18aa21b8a75182eb087c7648 Mon Sep 17 00:00:00 2001 From: J Date: Sat, 17 Oct 2026 03:27:26 +0530 Subject: [PATCH 2/2] Test ZIP option and wildcard entry names are rejected before extraction --- Tests/ZipInspectorTests.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/ZipInspectorTests.swift b/Tests/ZipInspectorTests.swift index 801a4f4..367f3d2 100644 --- a/Tests/ZipInspectorTests.swift +++ b/Tests/ZipInspectorTests.swift @@ -49,6 +49,19 @@ struct ZipInspectorTests { catch { rejected = true } assert(rejected, "Traversal must be refused before extraction.") } + // unzip interprets entry selections as argv options or filename globs. + // Refuse these at inspection instead of broadening a selective extract. + let original = [UInt8](try Data(contentsOf: archive)) + for byte in [UInt8(45), UInt8(42), UInt8(63), UInt8(91)] { + var edited = original + edited[start] = byte + let candidate = tmp.appendingPathComponent("selector-\(Int(byte)).zip") + try Data(edited).write(to: candidate) + var rejected = false + do { _ = try ZipInspector.inspect(candidate) } + catch { rejected = true } + assert(rejected, "Option-like or wildcard ZIP entry names must be rejected.") + } print("ZipInspectorTests passed") } }