@@ -230,13 +230,13 @@ private static bool ExtractRelativeToDirectoryCheckIfFile(ZipArchiveEntry source
230230
231231 fileDestinationPath = Path . GetFullPath ( Path . Combine ( destinationDirectoryFullPath , sanitizedEntryPath ) ) ;
232232
233- // Verify the resolved path stays within the destination root. On case-insensitive platforms
234- // (Windows, macOS, iOS, tvOS) the comparison below is OrdinalIgnoreCase, which alone cannot
235- // detect a ".." segment that pops the root's final segment and re-descends into a
236- // differently-cased sibling directory (e.g. "../dest/pwn.txt" extracted into a root named
237- // "Dest"). Path.GetFullPath never re-cases the segments we supplied, so a prefix that matches
238- // only case-insensitively is the signature of exactly that escape. Require an ordinal match of
239- // the root prefix on those platforms to reject it, while still allowing benign in-root ".." .
233+ // Reject entries that resolve outside the destination root. GetFullPath collapses "." and ".."
234+ // but never re-cases the segments it keeps. The root is combined in verbatim with a trailing
235+ // separator. That means a resolved path that shares the root's exact casing never climbed above the root;
236+ // one that matches the root only case-insensitively did climb out and re-descend under a
237+ // different spelling (e.g. "../dest/x" into root "Dest"), which is a distinct directory on a
238+ // case-sensitive volume. The two terms below together require that ordinal match. Symlinks and
239+ // junctions are not resolved here, so a reparse point inside the root is a separate concern .
240240 if ( ! fileDestinationPath . StartsWith ( destinationDirectoryFullPath , PathInternal . StringComparison ) ||
241241 ( ! PathInternal . IsCaseSensitive && ! fileDestinationPath . StartsWith ( destinationDirectoryFullPath , StringComparison . Ordinal ) ) )
242242 {
0 commit comments