@@ -16,13 +16,13 @@ use crate::types::{
16
16
use crate :: zipcrypto:: { ZipCryptoReader , ZipCryptoReaderValid , ZipCryptoValidator } ;
17
17
use indexmap:: IndexMap ;
18
18
use std:: borrow:: Cow ;
19
- use std:: ffi:: OsString ;
19
+ use std:: ffi:: OsStr ;
20
20
use std:: fs:: create_dir_all;
21
21
use std:: io:: { self , copy, prelude:: * , sink, SeekFrom } ;
22
22
use std:: mem;
23
23
use std:: mem:: size_of;
24
24
use std:: ops:: Deref ;
25
- use std:: path:: { Path , PathBuf } ;
25
+ use std:: path:: { Component , Path , PathBuf } ;
26
26
use std:: sync:: { Arc , OnceLock } ;
27
27
28
28
mod config;
@@ -318,6 +318,22 @@ impl<R: Read> Read for SeekableTake<'_, R> {
318
318
}
319
319
}
320
320
321
+ pub ( crate ) fn make_writable_dir_all < T : AsRef < Path > > ( outpath : T ) -> Result < ( ) , ZipError > {
322
+ create_dir_all ( outpath. as_ref ( ) ) ?;
323
+ #[ cfg( unix) ]
324
+ {
325
+ // Dirs must be writable until all normal files are extracted
326
+ use std:: os:: unix:: fs:: PermissionsExt ;
327
+ std:: fs:: set_permissions (
328
+ outpath. as_ref ( ) ,
329
+ std:: fs:: Permissions :: from_mode (
330
+ 0o700 | std:: fs:: metadata ( outpath. as_ref ( ) ) ?. permissions ( ) . mode ( ) ,
331
+ ) ,
332
+ ) ?;
333
+ }
334
+ Ok ( ( ) )
335
+ }
336
+
321
337
pub ( crate ) fn find_content < ' a > (
322
338
data : & ZipFileData ,
323
339
reader : & ' a mut ( impl Read + Seek ) ,
@@ -433,46 +449,41 @@ pub(crate) fn make_reader(
433
449
) ) ) )
434
450
}
435
451
436
- pub ( crate ) fn make_symlink < P : AsRef < Path > > ( directory : P , outpath : & PathBuf , target : Vec < u8 > ) -> ZipResult < ( ) > {
452
+ pub ( crate ) fn make_symlink ( outpath : & PathBuf , target : Vec < u8 > ) -> ZipResult < ( ) > {
437
453
#[ cfg( not( any( unix, windows) ) ) ]
438
454
{
439
455
let output = File :: create ( outpath. as_path ( ) ) ;
440
456
output. write_all ( target) ?;
441
457
continue ;
442
458
}
459
+
460
+ let Ok ( target) = String :: from_utf8 ( target) else {
461
+ return Err ( ZipError :: InvalidArchive ( "Invalid UTF-8 as symlink target" ) ) ;
462
+ } ;
463
+ let target = Path :: new ( & target) ;
464
+
443
465
#[ cfg( unix) ]
444
466
{
445
- use std:: os:: unix:: ffi:: OsStringExt ;
446
- let target = OsString :: from_vec ( target) ;
447
- let target_path = outpath. parent ( ) . unwrap ( ) . join ( & target) . canonicalize ( ) ?;
448
- if !target_path. starts_with ( directory. as_ref ( ) ) {
449
- return Err ( InvalidArchive ( "Symlink target would escape destination" ) ) ;
450
- }
451
- std:: os:: unix:: fs:: symlink ( & target, outpath. as_path ( ) ) ?;
467
+ std:: os:: unix:: fs:: symlink ( target, outpath. as_path ( ) ) ?;
452
468
}
453
469
#[ cfg( windows) ]
454
470
{
455
471
let Ok ( target) = String :: from_utf8 ( target) else {
456
472
return Err ( ZipError :: InvalidArchive ( "Invalid UTF-8 as symlink target" ) ) ;
457
473
} ;
458
474
let target = target. into_boxed_str ( ) ;
459
- let target_path = outpath. parent ( ) . unwrap ( ) . join ( & target) . canonicalize ( ) ?;
460
- if !target_path. canonicalize ( ) . starts_with ( directory) {
461
- return Err ( ZipError :: InvalidArchive ( "Symlink target would escape destination" ) ) ;
462
- }
463
- let target_is_dir_from_archive =
464
- self . shared . files . contains_key ( & target) && is_dir ( & target) ;
475
+ let target_is_dir_from_archive = self . shared . files . contains_key ( & target) && is_dir ( & target) ;
465
476
let target_is_dir = if target_is_dir_from_archive {
466
477
true
467
- } else if let Ok ( meta) = std:: fs:: metadata ( & target_path ) {
478
+ } else if let Ok ( meta) = std:: fs:: metadata ( & target ) {
468
479
meta. is_dir ( )
469
480
} else {
470
481
false
471
482
} ;
472
483
if target_is_dir {
473
- std:: os:: windows:: fs:: symlink_dir ( target_path , outpath. as_path ( ) ) ?;
484
+ std:: os:: windows:: fs:: symlink_dir ( target , outpath. as_path ( ) ) ?;
474
485
} else {
475
- std:: os:: windows:: fs:: symlink_file ( target_path , outpath. as_path ( ) ) ?;
486
+ std:: os:: windows:: fs:: symlink_file ( target , outpath. as_path ( ) ) ?;
476
487
}
477
488
}
478
489
Ok ( ( ) )
@@ -786,32 +797,26 @@ impl<R: Read + Seek> ZipArchive<R> {
786
797
let directory = directory. as_ref ( ) . canonicalize ( ) ?;
787
798
for i in 0 ..self . len ( ) {
788
799
let mut file = self . by_index ( i) ?;
789
- let filepath = file
790
- . enclosed_name ( )
791
- . ok_or ( InvalidArchive ( "Invalid file path" ) ) ?;
792
800
793
- let outpath = directory. join ( filepath) . canonicalize ( ) ?;
794
- if !outpath. starts_with ( & directory) {
795
- return Err ( InvalidArchive ( "File path is outside destination directory" ) )
796
- }
801
+ let mut outpath = directory. clone ( ) ;
802
+ file. safe_prepare_path ( & directory, & mut outpath) ?;
797
803
798
- if file. is_dir ( ) {
799
- Self :: make_writable_dir_all ( & outpath) ?;
800
- continue ;
801
- }
802
804
let symlink_target = if file. is_symlink ( ) && ( cfg ! ( unix) || cfg ! ( windows) ) {
803
805
let mut target = Vec :: with_capacity ( file. size ( ) as usize ) ;
804
806
file. read_to_end ( & mut target) ?;
805
807
Some ( target)
806
808
} else {
809
+ if file. is_dir ( ) {
810
+ crate :: read:: make_writable_dir_all ( & outpath) ?;
811
+ continue ;
812
+ }
807
813
None
808
814
} ;
815
+
809
816
drop ( file) ;
810
- if let Some ( p) = outpath. parent ( ) {
811
- Self :: make_writable_dir_all ( p) ?;
812
- }
817
+
813
818
if let Some ( target) = symlink_target {
814
- make_symlink ( & directory , & outpath, target) ?;
819
+ make_symlink ( & outpath, target) ?;
815
820
continue ;
816
821
}
817
822
let mut file = self . by_index ( i) ?;
@@ -841,22 +846,6 @@ impl<R: Read + Seek> ZipArchive<R> {
841
846
Ok ( ( ) )
842
847
}
843
848
844
- fn make_writable_dir_all < T : AsRef < Path > > ( outpath : T ) -> Result < ( ) , ZipError > {
845
- create_dir_all ( outpath. as_ref ( ) ) ?;
846
- #[ cfg( unix) ]
847
- {
848
- // Dirs must be writable until all normal files are extracted
849
- use std:: os:: unix:: fs:: PermissionsExt ;
850
- std:: fs:: set_permissions (
851
- outpath. as_ref ( ) ,
852
- std:: fs:: Permissions :: from_mode (
853
- 0o700 | std:: fs:: metadata ( outpath. as_ref ( ) ) ?. permissions ( ) . mode ( ) ,
854
- ) ,
855
- ) ?;
856
- }
857
- Ok ( ( ) )
858
- }
859
-
860
849
/// Number of files contained in this zip.
861
850
pub fn len ( & self ) -> usize {
862
851
self . shared . files . len ( )
@@ -1430,6 +1419,95 @@ impl<'a> ZipFile<'a> {
1430
1419
self . get_metadata ( ) . enclosed_name ( )
1431
1420
}
1432
1421
1422
+ pub ( crate ) fn simplified_components ( & self ) -> Option < Vec < & OsStr > > {
1423
+ self . get_metadata ( ) . simplified_components ( )
1424
+ }
1425
+
1426
+ /// Prepare the path for extraction by creating necessary missing directories and checking for symlinks to be contained within the base path.
1427
+ pub ( crate ) fn safe_prepare_path (
1428
+ & self ,
1429
+ base_path : & Path ,
1430
+ outpath : & mut PathBuf ,
1431
+ ) -> ZipResult < ( ) > {
1432
+ let components = self
1433
+ . simplified_components ( )
1434
+ . ok_or ( InvalidArchive ( "Invalid file path" ) ) ?;
1435
+
1436
+ let components_len = components. len ( ) ;
1437
+
1438
+ for ( is_last, component) in components
1439
+ . into_iter ( )
1440
+ . enumerate ( )
1441
+ . map ( |( i, c) | ( i == components_len - 1 , c) )
1442
+ {
1443
+ // we can skip the target directory itself because the base path is assumed to be "trusted" (if the user say extract to a symlink we can follow it)
1444
+ outpath. push ( component) ;
1445
+
1446
+ // check if the path is a symlink, the target must be _inherently_ within the directory
1447
+ for limit in ( 0 ..5u8 ) . rev ( ) {
1448
+ let meta = match std:: fs:: symlink_metadata ( & outpath) {
1449
+ Ok ( meta) => meta,
1450
+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => {
1451
+ if !is_last {
1452
+ crate :: read:: make_writable_dir_all ( & outpath) ?;
1453
+ }
1454
+ break ;
1455
+ }
1456
+ Err ( e) => return Err ( e. into ( ) ) ,
1457
+ } ;
1458
+
1459
+ if !meta. is_symlink ( ) {
1460
+ break ;
1461
+ }
1462
+
1463
+ if limit == 0 {
1464
+ return Err ( InvalidArchive ( "Symlink too deep" ) ) ;
1465
+ }
1466
+
1467
+ // note that we cannot accept links that do not inherently resolve to a path inside the directory to prevent:
1468
+ // - disclosure of unrelated path exists (no check for a path exist and then ../ out)
1469
+ // - issues with file-system specific path resolution (case sensitivity, etc)
1470
+ let target = std:: fs:: read_link ( & outpath) ?;
1471
+
1472
+ let is_relative_enclosed = target
1473
+ . components ( )
1474
+ . try_fold ( 0u32 , |acc, c| match c {
1475
+ Component :: Normal ( _) => acc. checked_add ( 1 ) ,
1476
+ Component :: ParentDir => acc. checked_sub ( 1 ) ,
1477
+ Component :: CurDir => Some ( acc) ,
1478
+ _ => None ,
1479
+ } )
1480
+ . is_some ( ) ;
1481
+
1482
+ if !is_relative_enclosed {
1483
+ let is_absolute_enclosed = base_path
1484
+ . components ( )
1485
+ . map ( Some )
1486
+ . chain ( std:: iter:: once ( None ) )
1487
+ . zip ( target. components ( ) . map ( Some ) . chain ( std:: iter:: repeat ( None ) ) )
1488
+ . all ( |( a, b) | match ( a, b) {
1489
+ // both components are normal
1490
+ ( Some ( Component :: Normal ( a) ) , Some ( Component :: Normal ( b) ) ) => a == b,
1491
+ // both components consumed fully
1492
+ ( None , None ) => true ,
1493
+ // target consumed fully but base path is not
1494
+ ( Some ( _) , None ) => false ,
1495
+ // base path consumed fully but target is not (and normal)
1496
+ ( None , Some ( Component :: CurDir | Component :: Normal ( _) ) ) => true ,
1497
+ _ => false ,
1498
+ } ) ;
1499
+
1500
+ if !is_absolute_enclosed {
1501
+ return Err ( InvalidArchive ( "Symlink is not inherently safe" ) ) ;
1502
+ }
1503
+ }
1504
+
1505
+ outpath. push ( target) ;
1506
+ }
1507
+ }
1508
+ Ok ( ( ) )
1509
+ }
1510
+
1433
1511
/// Get the comment of the file
1434
1512
pub fn comment ( & self ) -> & str {
1435
1513
& self . get_metadata ( ) . file_comment
@@ -1629,11 +1707,11 @@ pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<Zip
1629
1707
1630
1708
#[ cfg( test) ]
1631
1709
mod test {
1632
- use std:: fs:: remove_dir;
1633
1710
use crate :: result:: ZipResult ;
1634
1711
use crate :: write:: SimpleFileOptions ;
1635
1712
use crate :: CompressionMethod :: Stored ;
1636
1713
use crate :: { ZipArchive , ZipWriter } ;
1714
+ use std:: fs:: remove_dir;
1637
1715
use std:: io:: { Cursor , Read , Write } ;
1638
1716
use tempfile:: TempDir ;
1639
1717
@@ -1902,47 +1980,20 @@ mod test {
1902
1980
/// Symlinks being extracted shouldn't be followed out of the destination directory.
1903
1981
#[ test]
1904
1982
fn test_cannot_symlink_outside_destination ( ) -> ZipResult < ( ) > {
1905
- use std:: env:: temp_dir;
1906
1983
use std:: fs:: create_dir;
1907
1984
1908
1985
let mut writer = ZipWriter :: new ( Cursor :: new ( Vec :: new ( ) ) ) ;
1909
1986
writer. add_symlink ( "symlink/" , "../dest-sibling/" , SimpleFileOptions :: default ( ) ) ?;
1910
1987
writer. start_file ( "symlink/dest-file" , SimpleFileOptions :: default ( ) ) ?;
1911
1988
let mut reader = writer. finish_into_readable ( ) ?;
1912
- let dest_parent = temp_dir ( ) ;
1913
- let dest_sibling = dest_parent. join ( "dest-sibling" ) ;
1989
+ let dest_parent =
1990
+ TempDir :: with_prefix ( "read__test_cannot_symlink_outside_destination" ) . unwrap ( ) ;
1991
+ let dest_sibling = dest_parent. path ( ) . join ( "dest-sibling" ) ;
1914
1992
create_dir ( & dest_sibling) ?;
1915
- let dest = dest_parent. join ( "dest" ) ;
1993
+ let dest = dest_parent. path ( ) . join ( "dest" ) ;
1916
1994
create_dir ( & dest) ?;
1917
1995
assert ! ( reader. extract( dest) . is_err( ) ) ;
1918
1996
assert ! ( !dest_sibling. join( "dest-file" ) . exists( ) ) ;
1919
1997
Ok ( ( ) )
1920
1998
}
1921
-
1922
- #[ test]
1923
- fn test_cannot_create_symlink_loop ( ) -> ZipResult < ( ) > {
1924
- use std:: env:: temp_dir;
1925
- use std:: fs:: create_dir;
1926
- let mut writer = ZipWriter :: new ( Cursor :: new ( Vec :: new ( ) ) ) ;
1927
- writer. add_symlink ( "a/" , "b/x/" , SimpleFileOptions :: default ( ) ) ?;
1928
- writer. add_symlink ( "b/" , "a/x/" , SimpleFileOptions :: default ( ) ) ?;
1929
- let mut reader = writer. finish_into_readable ( ) ?;
1930
- let temp_dir = temp_dir ( ) . join ( "read_symlink_loop" ) ;
1931
- create_dir ( & temp_dir) ?;
1932
- assert ! ( reader. extract( & temp_dir) . is_err( ) ) ;
1933
- let _ = remove_dir ( temp_dir. join ( "a" ) ) ;
1934
- let _ = remove_dir ( temp_dir. join ( "b" ) ) ;
1935
- create_dir ( temp_dir. join ( "a" ) ) ?;
1936
- assert ! ( reader. extract( & temp_dir) . is_err( ) ) ;
1937
- let _ = remove_dir ( temp_dir. join ( "a" ) ) ;
1938
- let _ = remove_dir ( temp_dir. join ( "b" ) ) ;
1939
- create_dir ( temp_dir. join ( "b" ) ) ?;
1940
- assert ! ( reader. extract( & temp_dir) . is_err( ) ) ;
1941
- let _ = remove_dir ( temp_dir. join ( "a" ) ) ;
1942
- let _ = remove_dir ( temp_dir. join ( "b" ) ) ;
1943
- create_dir ( temp_dir. join ( "a" ) ) ?;
1944
- create_dir ( temp_dir. join ( "b" ) ) ?;
1945
- assert ! ( reader. extract( & temp_dir) . is_err( ) ) ;
1946
- Ok ( ( ) )
1947
- }
1948
1999
}
0 commit comments