@@ -120,6 +120,8 @@ impl Installer {
120120 }
121121
122122 /// Extract a tar.gz archive.
123+ ///
124+ /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions.
123125 async fn extract_tar_gz ( & self , archive_path : & Path , dest : & Path ) -> UpdateResult < PathBuf > {
124126 use flate2:: read:: GzDecoder ;
125127 use std:: fs:: File ;
@@ -129,13 +131,14 @@ impl Installer {
129131 let gz = GzDecoder :: new ( file) ;
130132 let mut archive = Archive :: new ( gz) ;
131133
132- // Validate paths before extraction to prevent path traversal attacks
133- for entry in archive. entries ( ) . map_err ( |e| UpdateError :: ExtractionFailed {
134+ // Single-pass: validate and extract each entry to prevent TOCTOU race conditions
135+ for entry_result in archive. entries ( ) . map_err ( |e| UpdateError :: ExtractionFailed {
134136 message : e. to_string ( ) ,
135137 } ) ? {
136- let entry = entry . map_err ( |e| UpdateError :: ExtractionFailed {
138+ let mut entry = entry_result . map_err ( |e| UpdateError :: ExtractionFailed {
137139 message : e. to_string ( ) ,
138140 } ) ?;
141+
139142 let path = entry. path ( ) . map_err ( |e| UpdateError :: ExtractionFailed {
140143 message : e. to_string ( ) ,
141144 } ) ?;
@@ -149,62 +152,79 @@ impl Installer {
149152 message : format ! ( "Path traversal detected in archive: {}" , path. display( ) ) ,
150153 } ) ;
151154 }
152- }
153-
154- // Re-open and extract after validation
155- let file = File :: open ( archive_path) ?;
156- let gz = GzDecoder :: new ( file) ;
157- let mut archive = Archive :: new ( gz) ;
158155
159- archive
160- . unpack ( dest)
161- . map_err ( |e| UpdateError :: ExtractionFailed {
162- message : e. to_string ( ) ,
163- } ) ?;
156+ // Extract the entry immediately after validation
157+ entry
158+ . unpack_in ( dest)
159+ . map_err ( |e| UpdateError :: ExtractionFailed {
160+ message : e. to_string ( ) ,
161+ } ) ?;
162+ }
164163
165164 self . find_binary ( dest) . await
166165 }
167166
168167 /// Extract a zip archive.
168+ ///
169+ /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions.
169170 async fn extract_zip ( & self , archive_path : & Path , dest : & Path ) -> UpdateResult < PathBuf > {
170171 let file = std:: fs:: File :: open ( archive_path) ?;
171172 let mut archive =
172173 zip:: ZipArchive :: new ( file) . map_err ( |e| UpdateError :: ExtractionFailed {
173174 message : e. to_string ( ) ,
174175 } ) ?;
175176
176- // Validate all paths before extraction to prevent path traversal attacks
177+ // Single-pass: validate and extract each entry to prevent TOCTOU race conditions
177178 for i in 0 ..archive. len ( ) {
178- let file = archive
179+ let mut zip_file = archive
179180 . by_index ( i)
180181 . map_err ( |e| UpdateError :: ExtractionFailed {
181182 message : e. to_string ( ) ,
182183 } ) ?;
183- let path = std:: path:: Path :: new ( file. name ( ) ) ;
184+
185+ let path = std:: path:: Path :: new ( zip_file. name ( ) ) ;
184186
185187 // Check for path traversal
186188 if path
187189 . components ( )
188190 . any ( |c| matches ! ( c, std:: path:: Component :: ParentDir ) )
189191 {
190192 return Err ( UpdateError :: ExtractionFailed {
191- message : format ! ( "Path traversal detected in archive: {}" , file . name( ) ) ,
193+ message : format ! ( "Path traversal detected in archive: {}" , zip_file . name( ) ) ,
192194 } ) ;
193195 }
194- }
195196
196- // Re-open and extract after validation
197- let file = std:: fs:: File :: open ( archive_path) ?;
198- let mut archive =
199- zip:: ZipArchive :: new ( file) . map_err ( |e| UpdateError :: ExtractionFailed {
200- message : e. to_string ( ) ,
201- } ) ?;
197+ // Determine the output path
198+ let outpath = dest. join ( zip_file. name ( ) ) ;
202199
203- archive
204- . extract ( dest)
205- . map_err ( |e| UpdateError :: ExtractionFailed {
206- message : e. to_string ( ) ,
207- } ) ?;
200+ // Extract the entry immediately after validation
201+ if zip_file. is_dir ( ) {
202+ std:: fs:: create_dir_all ( & outpath) ?;
203+ } else {
204+ // Ensure parent directory exists
205+ if let Some ( parent) = outpath. parent ( ) {
206+ std:: fs:: create_dir_all ( parent) ?;
207+ }
208+ let mut outfile =
209+ std:: fs:: File :: create ( & outpath) . map_err ( |e| UpdateError :: ExtractionFailed {
210+ message : format ! ( "Failed to create file {}: {}" , outpath. display( ) , e) ,
211+ } ) ?;
212+ std:: io:: copy ( & mut zip_file, & mut outfile) . map_err ( |e| {
213+ UpdateError :: ExtractionFailed {
214+ message : format ! ( "Failed to extract {}: {}" , outpath. display( ) , e) ,
215+ }
216+ } ) ?;
217+
218+ // Set file permissions on Unix
219+ #[ cfg( unix) ]
220+ {
221+ use std:: os:: unix:: fs:: PermissionsExt ;
222+ if let Some ( mode) = zip_file. unix_mode ( ) {
223+ std:: fs:: set_permissions ( & outpath, std:: fs:: Permissions :: from_mode ( mode) ) ?;
224+ }
225+ }
226+ }
227+ }
208228
209229 self . find_binary ( dest) . await
210230 }
@@ -328,6 +348,7 @@ impl Installer {
328348}
329349
330350/// Check if we have permission to write to the installation directory.
351+ #[ allow( dead_code) ]
331352pub fn check_write_permission ( ) -> UpdateResult < ( ) > {
332353 let exe_path = std:: env:: current_exe ( ) ?;
333354 let parent = exe_path
0 commit comments