Skip to content

Commit 977bb94

Browse files
fix failing tests, remove symlink loop check
Signed-off-by: eternal-flame-AD <[email protected]>
1 parent 3cb29e7 commit 977bb94

File tree

3 files changed

+176
-133
lines changed

3 files changed

+176
-133
lines changed

src/read.rs

Lines changed: 133 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ use crate::types::{
1616
use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
1717
use indexmap::IndexMap;
1818
use std::borrow::Cow;
19-
use std::ffi::OsString;
19+
use std::ffi::OsStr;
2020
use std::fs::create_dir_all;
2121
use std::io::{self, copy, prelude::*, sink, SeekFrom};
2222
use std::mem;
2323
use std::mem::size_of;
2424
use std::ops::Deref;
25-
use std::path::{Path, PathBuf};
25+
use std::path::{Component, Path, PathBuf};
2626
use std::sync::{Arc, OnceLock};
2727

2828
mod config;
@@ -318,6 +318,22 @@ impl<R: Read> Read for SeekableTake<'_, R> {
318318
}
319319
}
320320

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+
321337
pub(crate) fn find_content<'a>(
322338
data: &ZipFileData,
323339
reader: &'a mut (impl Read + Seek),
@@ -433,46 +449,41 @@ pub(crate) fn make_reader(
433449
))))
434450
}
435451

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<()> {
437453
#[cfg(not(any(unix, windows)))]
438454
{
439455
let output = File::create(outpath.as_path());
440456
output.write_all(target)?;
441457
continue;
442458
}
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+
443465
#[cfg(unix)]
444466
{
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())?;
452468
}
453469
#[cfg(windows)]
454470
{
455471
let Ok(target) = String::from_utf8(target) else {
456472
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
457473
};
458474
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);
465476
let target_is_dir = if target_is_dir_from_archive {
466477
true
467-
} else if let Ok(meta) = std::fs::metadata(&target_path) {
478+
} else if let Ok(meta) = std::fs::metadata(&target) {
468479
meta.is_dir()
469480
} else {
470481
false
471482
};
472483
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())?;
474485
} else {
475-
std::os::windows::fs::symlink_file(target_path, outpath.as_path())?;
486+
std::os::windows::fs::symlink_file(target, outpath.as_path())?;
476487
}
477488
}
478489
Ok(())
@@ -786,32 +797,26 @@ impl<R: Read + Seek> ZipArchive<R> {
786797
let directory = directory.as_ref().canonicalize()?;
787798
for i in 0..self.len() {
788799
let mut file = self.by_index(i)?;
789-
let filepath = file
790-
.enclosed_name()
791-
.ok_or(InvalidArchive("Invalid file path"))?;
792800

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)?;
797803

798-
if file.is_dir() {
799-
Self::make_writable_dir_all(&outpath)?;
800-
continue;
801-
}
802804
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
803805
let mut target = Vec::with_capacity(file.size() as usize);
804806
file.read_to_end(&mut target)?;
805807
Some(target)
806808
} else {
809+
if file.is_dir() {
810+
crate::read::make_writable_dir_all(&outpath)?;
811+
continue;
812+
}
807813
None
808814
};
815+
809816
drop(file);
810-
if let Some(p) = outpath.parent() {
811-
Self::make_writable_dir_all(p)?;
812-
}
817+
813818
if let Some(target) = symlink_target {
814-
make_symlink(&directory, &outpath, target)?;
819+
make_symlink(&outpath, target)?;
815820
continue;
816821
}
817822
let mut file = self.by_index(i)?;
@@ -841,22 +846,6 @@ impl<R: Read + Seek> ZipArchive<R> {
841846
Ok(())
842847
}
843848

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-
860849
/// Number of files contained in this zip.
861850
pub fn len(&self) -> usize {
862851
self.shared.files.len()
@@ -1430,6 +1419,95 @@ impl<'a> ZipFile<'a> {
14301419
self.get_metadata().enclosed_name()
14311420
}
14321421

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+
14331511
/// Get the comment of the file
14341512
pub fn comment(&self) -> &str {
14351513
&self.get_metadata().file_comment
@@ -1629,11 +1707,11 @@ pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<Zip
16291707

16301708
#[cfg(test)]
16311709
mod test {
1632-
use std::fs::remove_dir;
16331710
use crate::result::ZipResult;
16341711
use crate::write::SimpleFileOptions;
16351712
use crate::CompressionMethod::Stored;
16361713
use crate::{ZipArchive, ZipWriter};
1714+
use std::fs::remove_dir;
16371715
use std::io::{Cursor, Read, Write};
16381716
use tempfile::TempDir;
16391717

@@ -1902,47 +1980,20 @@ mod test {
19021980
/// Symlinks being extracted shouldn't be followed out of the destination directory.
19031981
#[test]
19041982
fn test_cannot_symlink_outside_destination() -> ZipResult<()> {
1905-
use std::env::temp_dir;
19061983
use std::fs::create_dir;
19071984

19081985
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
19091986
writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
19101987
writer.start_file("symlink/dest-file", SimpleFileOptions::default())?;
19111988
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");
19141992
create_dir(&dest_sibling)?;
1915-
let dest = dest_parent.join("dest");
1993+
let dest = dest_parent.path().join("dest");
19161994
create_dir(&dest)?;
19171995
assert!(reader.extract(dest).is_err());
19181996
assert!(!dest_sibling.join("dest-file").exists());
19191997
Ok(())
19201998
}
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-
}
19481999
}

0 commit comments

Comments
 (0)