1- use std:: path:: Path ;
1+ use std:: path:: { Component , Path , PathBuf } ;
22use std:: pin:: Pin ;
33
4- use crate :: Error ;
54use futures:: StreamExt ;
65use rustc_hash:: FxHashSet ;
76use tokio_util:: compat:: { FuturesAsyncReadCompatExt , TokioAsyncReadCompatExt } ;
87use tracing:: warn;
8+
99use uv_distribution_filename:: SourceDistExtension ;
1010
11+ use crate :: Error ;
12+
1113const DEFAULT_BUF_SIZE : usize = 128 * 1024 ;
1214
1315/// Unpack a `.zip` archive into the target directory, without requiring `Seek`.
@@ -19,6 +21,26 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
1921 reader : R ,
2022 target : impl AsRef < Path > ,
2123) -> Result < ( ) , Error > {
24+ /// Ensure the file path is safe to use as a [`Path`].
25+ ///
26+ /// See: <https://docs.rs/zip/latest/zip/read/struct.ZipFile.html#method.enclosed_name>
27+ pub ( crate ) fn enclosed_name ( file_name : & str ) -> Option < PathBuf > {
28+ if file_name. contains ( '\0' ) {
29+ return None ;
30+ }
31+ let path = PathBuf :: from ( file_name) ;
32+ let mut depth = 0usize ;
33+ for component in path. components ( ) {
34+ match component {
35+ Component :: Prefix ( _) | Component :: RootDir => return None ,
36+ Component :: ParentDir => depth = depth. checked_sub ( 1 ) ?,
37+ Component :: Normal ( _) => depth += 1 ,
38+ Component :: CurDir => ( ) ,
39+ }
40+ }
41+ Some ( path)
42+ }
43+
2244 let target = target. as_ref ( ) ;
2345 let mut reader = futures:: io:: BufReader :: with_capacity ( DEFAULT_BUF_SIZE , reader. compat ( ) ) ;
2446 let mut zip = async_zip:: base:: read:: stream:: ZipFileReader :: new ( & mut reader) ;
@@ -28,6 +50,16 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
2850 while let Some ( mut entry) = zip. next_with_entry ( ) . await ? {
2951 // Construct the (expected) path to the file on-disk.
3052 let path = entry. reader ( ) . entry ( ) . filename ( ) . as_str ( ) ?;
53+
54+ // Sanitize the file name to prevent directory traversal attacks.
55+ let Some ( path) = enclosed_name ( path) else {
56+ warn ! ( "Skipping unsafe file name: {path}" ) ;
57+
58+ // Close current file prior to proceeding, as per:
59+ // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/
60+ zip = entry. skip ( ) . await ?;
61+ continue ;
62+ } ;
3163 let path = target. join ( path) ;
3264 let is_dir = entry. reader ( ) . entry ( ) . dir ( ) ?;
3365
@@ -55,7 +87,7 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
5587 tokio:: io:: copy ( & mut reader, & mut writer) . await ?;
5688 }
5789
58- // Close current file to get access to the next one. See docs :
90+ // Close current file prior to proceeding, as per :
5991 // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/
6092 zip = entry. skip ( ) . await ?;
6193 }
@@ -84,6 +116,9 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
84116 if has_any_executable_bit != 0 {
85117 // Construct the (expected) path to the file on-disk.
86118 let path = entry. filename ( ) . as_str ( ) ?;
119+ let Some ( path) = enclosed_name ( path) else {
120+ continue ;
121+ } ;
87122 let path = target. join ( path) ;
88123
89124 let permissions = fs_err:: tokio:: metadata ( & path) . await ?. permissions ( ) ;
0 commit comments