1- use std:: path:: { Path , PathBuf } ;
1+ use std:: path:: { Component , Path , PathBuf } ;
22use std:: pin:: Pin ;
33
44use futures:: StreamExt ;
@@ -21,22 +21,24 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
2121 reader : R ,
2222 target : impl AsRef < Path > ,
2323) -> Result < ( ) , Error > {
24- /// Sanitize a filename for use on Windows.
25- fn sanitize ( filename : & str ) -> PathBuf {
26- filename
27- . replace ( '\\' , "/" )
28- . split ( '/' )
29- . map ( |segment| {
30- sanitize_filename:: sanitize_with_options (
31- segment,
32- sanitize_filename:: Options {
33- windows : cfg ! ( windows) ,
34- truncate : false ,
35- replacement : "" ,
36- } ,
37- )
38- } )
39- . collect ( )
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)
4042 }
4143
4244 let target = target. as_ref ( ) ;
@@ -48,7 +50,17 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
4850 while let Some ( mut entry) = zip. next_with_entry ( ) . await ? {
4951 // Construct the (expected) path to the file on-disk.
5052 let path = entry. reader ( ) . entry ( ) . filename ( ) . as_str ( ) ?;
51- let path = target. join ( sanitize ( path) ) ;
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+ } ;
63+ let path = target. join ( path) ;
5264 let is_dir = entry. reader ( ) . entry ( ) . dir ( ) ?;
5365
5466 // Either create the directory or write the file to disk.
@@ -75,7 +87,7 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
7587 tokio:: io:: copy ( & mut reader, & mut writer) . await ?;
7688 }
7789
78- // Close current file to get access to the next one. See docs :
90+ // Close current file prior to proceeding, as per :
7991 // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/
8092 zip = entry. skip ( ) . await ?;
8193 }
@@ -104,7 +116,10 @@ pub async fn unzip<R: tokio::io::AsyncRead + Unpin>(
104116 if has_any_executable_bit != 0 {
105117 // Construct the (expected) path to the file on-disk.
106118 let path = entry. filename ( ) . as_str ( ) ?;
107- let path = target. join ( sanitize ( path) ) ;
119+ let Some ( path) = enclosed_name ( path) else {
120+ continue ;
121+ } ;
122+ let path = target. join ( path) ;
108123
109124 let permissions = fs_err:: tokio:: metadata ( & path) . await ?. permissions ( ) ;
110125 if permissions. mode ( ) & 0o111 != 0o111 {
0 commit comments