@@ -4,7 +4,7 @@ use std::fs;
44use std:: io:: prelude:: * ;
55use std:: io:: { self , Error , ErrorKind , SeekFrom } ;
66use std:: marker;
7- use std:: path:: { Component , Path } ;
7+ use std:: path:: { Component , Path , PathBuf } ;
88
99use filetime:: { self , FileTime } ;
1010
@@ -369,37 +369,14 @@ impl<'a> EntryFields<'a> {
369369 None => return Ok ( false ) ,
370370 } ;
371371
372- if ! parent. exists ( ) {
372+ if parent. symlink_metadata ( ) . is_err ( ) {
373373 fs:: create_dir_all ( & parent) . map_err ( |e| {
374374 TarError :: new ( & format ! ( "failed to create `{}`" ,
375375 parent. display( ) ) , e)
376376 } ) ?;
377377 }
378378
379- // Abort if target (canonical) parent is outside of `dst`
380- let canon_parent = parent. canonicalize ( ) . map_err ( |err| {
381- Error :: new (
382- err. kind ( ) ,
383- format ! ( "{} while canonicalizing {}" , err, parent. display( ) ) ,
384- )
385- } ) ?;
386- let canon_target = dst. canonicalize ( ) . map_err ( |err| {
387- Error :: new (
388- err. kind ( ) ,
389- format ! ( "{} while canonicalizing {}" , err, dst. display( ) ) ,
390- )
391- } ) ?;
392- if !canon_parent. starts_with ( & canon_target) {
393- let err = TarError :: new (
394- & format ! (
395- "trying to unpack outside of destination path: {}" ,
396- canon_target. display( )
397- ) ,
398- // TODO: use ErrorKind::InvalidInput here? (minor breaking change)
399- Error :: new ( ErrorKind :: Other , "Invalid argument" ) ,
400- ) ;
401- return Err ( err. into ( ) ) ;
402- }
379+ let canon_target = self . validate_inside_dst ( & dst, parent) ?;
403380
404381 self . unpack ( Some ( & canon_target) , & file_dst) . map_err ( |e| {
405382 TarError :: new ( & format ! ( "failed to unpack `{}`" , file_dst. display( ) ) , e)
@@ -441,8 +418,23 @@ impl<'a> EntryFields<'a> {
441418
442419 return if kind. is_hard_link ( ) {
443420 let link_src = match target_base {
421+ // If we're unpacking within a directory then ensure that
422+ // the destination of this hard link is both present and
423+ // inside our own directory. This is needed because we want
424+ // to make sure to not overwrite anything outside the root.
425+ //
426+ // Note that this logic is only needed for hard links
427+ // currently. With symlinks the `validate_inside_dst` which
428+ // happens before this method as part of `unpack_in` will
429+ // use canonicalization to ensure this guarantee. For hard
430+ // links though they're canonicalized to their existing path
431+ // so we need to validate at this time.
432+ Some ( ref p) => {
433+ let link_src = p. join ( src) ;
434+ self . validate_inside_dst ( p, & link_src) ?;
435+ link_src
436+ }
444437 None => src. into_owned ( ) ,
445- Some ( ref p) => p. join ( src) ,
446438 } ;
447439 fs:: hard_link ( & link_src, dst) . map_err ( |err| Error :: new (
448440 err. kind ( ) ,
@@ -490,7 +482,16 @@ impl<'a> EntryFields<'a> {
490482 // As a result if we don't recognize the kind we just write out the file
491483 // as we would normally.
492484
493- fs:: File :: create ( dst) . and_then ( |mut f| {
485+ // Remove an existing file, if any, to avoid writing through
486+ // symlinks/hardlinks to weird locations. The tar archive says this is a
487+ // regular file, so let's make it a regular file.
488+ ( || -> io:: Result < ( ) > {
489+ match fs:: remove_file ( dst) {
490+ Ok ( ( ) ) => { }
491+ Err ( ref e) if e. kind ( ) == io:: ErrorKind :: NotFound => { }
492+ Err ( e) => return Err ( e)
493+ }
494+ let mut f = fs:: File :: create ( dst) ?;
494495 for io in self . data . drain ( ..) {
495496 match io {
496497 EntryIo :: Data ( mut d) => {
@@ -508,7 +509,7 @@ impl<'a> EntryFields<'a> {
508509 }
509510 }
510511 Ok ( ( ) )
511- } ) . map_err ( |e| {
512+ } ) ( ) . map_err ( |e| {
512513 let header = self . header . path_bytes ( ) ;
513514 TarError :: new ( & format ! ( "failed to unpack `{}` into `{}`" ,
514515 String :: from_utf8_lossy( & header) ,
@@ -596,6 +597,34 @@ impl<'a> EntryFields<'a> {
596597 Ok ( ( ) )
597598 }
598599 }
600+
601+ fn validate_inside_dst ( & self , dst : & Path , file_dst : & Path ) -> io:: Result < PathBuf > {
602+ // Abort if target (canonical) parent is outside of `dst`
603+ let canon_parent = file_dst. canonicalize ( ) . map_err ( |err| {
604+ Error :: new (
605+ err. kind ( ) ,
606+ format ! ( "{} while canonicalizing {}" , err, file_dst. display( ) ) ,
607+ )
608+ } ) ?;
609+ let canon_target = dst. canonicalize ( ) . map_err ( |err| {
610+ Error :: new (
611+ err. kind ( ) ,
612+ format ! ( "{} while canonicalizing {}" , err, dst. display( ) ) ,
613+ )
614+ } ) ?;
615+ if !canon_parent. starts_with ( & canon_target) {
616+ let err = TarError :: new (
617+ & format ! (
618+ "trying to unpack outside of destination path: {}" ,
619+ canon_target. display( )
620+ ) ,
621+ // TODO: use ErrorKind::InvalidInput here? (minor breaking change)
622+ Error :: new ( ErrorKind :: Other , "Invalid argument" ) ,
623+ ) ;
624+ return Err ( err. into ( ) ) ;
625+ }
626+ Ok ( canon_target)
627+ }
599628}
600629
601630impl < ' a > Read for EntryFields < ' a > {
0 commit comments