Skip to content

Commit 54651a8

Browse files
authored
Merge pull request #156 from alexcrichton/protect-hard-links
Don't allow hardlinks to overwrite existing files
2 parents c7f3b8d + d3d14ad commit 54651a8

File tree

2 files changed

+129
-29
lines changed

2 files changed

+129
-29
lines changed

src/entry.rs

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs;
44
use std::io::prelude::*;
55
use std::io::{self, Error, ErrorKind, SeekFrom};
66
use std::marker;
7-
use std::path::{Component, Path};
7+
use std::path::{Component, Path, PathBuf};
88

99
use 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

601630
impl<'a> Read for EntryFields<'a> {

tests/entry.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ extern crate tar;
22
extern crate tempdir;
33

44
use std::fs::File;
5+
use std::io::Read;
56

67
use tempdir::TempDir;
78

@@ -247,3 +248,73 @@ fn good_parent_paths_ok() {
247248
let dst = t!(td.path().join("foo").join("bar").canonicalize());
248249
t!(File::open(dst));
249250
}
251+
252+
#[test]
253+
fn modify_hard_link_just_created() {
254+
let mut ar = tar::Builder::new(Vec::new());
255+
256+
let mut header = tar::Header::new_gnu();
257+
header.set_size(0);
258+
header.set_entry_type(tar::EntryType::Link);
259+
t!(header.set_path("foo"));
260+
t!(header.set_link_name("../test"));
261+
header.set_cksum();
262+
t!(ar.append(&header, &[][..]));
263+
264+
let mut header = tar::Header::new_gnu();
265+
header.set_size(1);
266+
header.set_entry_type(tar::EntryType::Regular);
267+
t!(header.set_path("foo"));
268+
header.set_cksum();
269+
t!(ar.append(&header, &b"x"[..]));
270+
271+
let bytes = t!(ar.into_inner());
272+
let mut ar = tar::Archive::new(&bytes[..]);
273+
274+
let td = t!(TempDir::new("tar"));
275+
276+
let test = td.path().join("test");
277+
t!(File::create(&test));
278+
279+
let dir = td.path().join("dir");
280+
assert!(ar.unpack(&dir).is_err());
281+
282+
let mut contents = Vec::new();
283+
t!(t!(File::open(&test)).read_to_end(&mut contents));
284+
assert_eq!(contents.len(), 0);
285+
}
286+
287+
#[test]
288+
fn modify_symlink_just_created() {
289+
let mut ar = tar::Builder::new(Vec::new());
290+
291+
let mut header = tar::Header::new_gnu();
292+
header.set_size(0);
293+
header.set_entry_type(tar::EntryType::Symlink);
294+
t!(header.set_path("foo"));
295+
t!(header.set_link_name("../test"));
296+
header.set_cksum();
297+
t!(ar.append(&header, &[][..]));
298+
299+
let mut header = tar::Header::new_gnu();
300+
header.set_size(1);
301+
header.set_entry_type(tar::EntryType::Regular);
302+
t!(header.set_path("foo"));
303+
header.set_cksum();
304+
t!(ar.append(&header, &b"x"[..]));
305+
306+
let bytes = t!(ar.into_inner());
307+
let mut ar = tar::Archive::new(&bytes[..]);
308+
309+
let td = t!(TempDir::new("tar"));
310+
311+
let test = td.path().join("test");
312+
t!(File::create(&test));
313+
314+
let dir = td.path().join("dir");
315+
t!(ar.unpack(&dir));
316+
317+
let mut contents = Vec::new();
318+
t!(t!(File::open(&test)).read_to_end(&mut contents));
319+
assert_eq!(contents.len(), 0);
320+
}

0 commit comments

Comments
 (0)