Skip to content

Commit fe5def1

Browse files
author
curious-rabbit
committed
security improvements
1 parent a009b51 commit fe5def1

14 files changed

Lines changed: 332 additions & 68 deletions

File tree

src/archive/rar.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::{
88
error::{Error, Result},
99
info,
1010
list::{FileInArchive, ListFileType},
11-
utils::BytesFmt,
11+
utils::{BytesFmt, PathFmt, validate_entry_path},
12+
warning,
1213
};
1314

1415
/// Unpacks the archive given by `archive_path` into the folder given by `output_folder`.
@@ -25,13 +26,18 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio
2526
while let Some(header) = archive.read_header()? {
2627
let entry = header.entry();
2728
archive = if entry.is_file() {
28-
info!(
29-
"extracted ({}) {}",
30-
BytesFmt(entry.unpacked_size),
31-
entry.filename.display(),
32-
);
33-
files_unpacked += 1;
34-
header.extract_with_base(output_folder)?
29+
if let Err(e) = validate_entry_path(&entry.filename) {
30+
warning!("skipping unsafe rar entry {}: {}", PathFmt(&entry.filename), e);
31+
header.skip()?
32+
} else {
33+
info!(
34+
"extracted ({}) {}",
35+
BytesFmt(entry.unpacked_size),
36+
PathFmt(&entry.filename),
37+
);
38+
files_unpacked += 1;
39+
header.extract_with_base(output_folder)?
40+
}
3541
} else {
3642
header.skip()?
3743
};

src/archive/sevenz.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! SevenZip archive format compress function
22
33
use std::{
4-
env,
54
io::{self, BufWriter, Read, Seek, Write},
65
path::{Path, PathBuf},
76
};
@@ -18,7 +17,9 @@ use crate::{
1817
info,
1918
list::{FileInArchive, ListFileType},
2019
utils::{
21-
BytesFmt, FileVisibilityPolicy, PathFmt, cd_into_same_dir_as, ensure_parent_dir_exists, is_same_file_as_output,
20+
BytesFmt, FileVisibilityPolicy, LimitedReader, PathFmt, SanitizedStr, cd_into_same_dir_as,
21+
ensure_parent_dir_exists, is_same_file_as_output, max_decompressed_bytes, validate_dest_inside_root,
22+
validate_entry_path,
2223
},
2324
warning,
2425
};
@@ -33,10 +34,27 @@ where
3334
|entry: &ArchiveEntry, reader: &mut dyn Read, path: &PathBuf| -> Result<bool, sevenz_rust2::Error> {
3435
// Manually handle writing all files from 7z archive (the library defaults ignore empty files)
3536

36-
let file_path = output_path.join(entry.name());
37+
let name_as_path = std::path::Path::new(entry.name());
38+
let safe_relpath = match validate_entry_path(name_as_path) {
39+
Ok(p) => p,
40+
Err(e) => {
41+
warning!("skipping unsafe 7z entry {}: {}", PathFmt(name_as_path), e);
42+
return Ok(true);
43+
}
44+
};
45+
let file_path = output_path.join(&safe_relpath);
46+
47+
if let Err(e) = validate_dest_inside_root(output_path, &file_path) {
48+
warning!("skipping 7z entry {}: {}", PathFmt(&file_path), e);
49+
return Ok(true);
50+
}
3751

3852
if entry.is_directory() {
39-
info!("File {} extracted to {}", entry.name(), PathFmt(&file_path));
53+
info!(
54+
"File {} extracted to {}",
55+
SanitizedStr(entry.name()),
56+
PathFmt(&file_path)
57+
);
4058
if !path.fs_err_try_exists()? {
4159
fs::create_dir_all(path)?;
4260
}
@@ -47,7 +65,8 @@ where
4765

4866
let file = fs::File::create(path)?;
4967
let mut writer = BufWriter::new(file);
50-
io::copy(reader, &mut writer)?;
68+
let mut limited = LimitedReader::new(reader, max_decompressed_bytes());
69+
io::copy(&mut limited, &mut writer)?;
5170

5271
use filetime_creation as ft;
5372
// Surface mtime-set failures as warnings so users know timestamps weren't preserved
@@ -136,6 +155,7 @@ where
136155

137156
for filename in files {
138157
let previous_location = cd_into_same_dir_as(filename)?;
158+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
139159

140160
// Unwrap safety:
141161
// paths should be canonicalized by now, and the root directory rejected.
@@ -172,8 +192,6 @@ where
172192

173193
writer.push_archive_entry::<fs::File>(entry, entry_data)?;
174194
}
175-
176-
env::set_current_dir(previous_location)?;
177195
}
178196

179197
let bytes = writer.finish()?;

src/archive/tar.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use std::os::unix::fs::MetadataExt;
55
use std::{
66
collections::HashMap,
7-
env,
87
io::{self, prelude::*},
98
ops::Not,
109
path::{Path, PathBuf},
@@ -20,7 +19,8 @@ use crate::{
2019
list::{FileInArchive, ListFileType},
2120
utils::{
2221
self, BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, create_symlink, is_same_file_as_output,
23-
read_file_type, set_permission_mode,
22+
read_file_type, sanitize_archive_mode, set_permission_mode, validate_dest_inside_root, validate_entry_path,
23+
validate_symlink_target,
2424
},
2525
warning,
2626
};
@@ -38,23 +38,31 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path) -> Result<u64> {
3838

3939
match entry.header().entry_type() {
4040
tar::EntryType::Symlink => {
41-
let relative_path = entry.path()?;
42-
let full_path = output_folder.join(&relative_path);
41+
let raw_path = entry.path()?.into_owned();
42+
let safe_relpath = validate_entry_path(&raw_path)?;
43+
let full_path = output_folder.join(&safe_relpath);
4344
let target = entry
4445
.link_name()?
4546
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing symlink target"))?;
4647

48+
validate_symlink_target(&safe_relpath, &target)?;
49+
validate_dest_inside_root(output_folder, &full_path)?;
4750
create_symlink(&target, &full_path)?;
4851
}
4952
tar::EntryType::Link => {
50-
let link_path = entry.path()?;
51-
let target = entry
53+
let raw_link = entry.path()?.into_owned();
54+
let safe_link_path = validate_entry_path(&raw_link)?;
55+
let raw_target = entry
5256
.link_name()?
53-
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing hardlink target"))?;
57+
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing hardlink target"))?
58+
.into_owned();
59+
let safe_target = validate_entry_path(&raw_target)?;
5460

55-
let full_link_path = output_folder.join(&link_path);
56-
let full_target_path = output_folder.join(&target);
61+
let full_link_path = output_folder.join(&safe_link_path);
62+
let full_target_path = output_folder.join(&safe_target);
5763

64+
validate_dest_inside_root(output_folder, &full_link_path)?;
65+
validate_dest_inside_root(output_folder, &full_target_path)?;
5866
fs::hard_link(&full_target_path, &full_link_path)?;
5967
}
6068
tar::EntryType::Regular | tar::EntryType::GNUSparse => {
@@ -71,10 +79,11 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path) -> Result<u64> {
7179
// We unpacked a read-only directory, make it writeable so that we can
7280
// create the files inside of it, by the end, restore the original mode
7381
let original_path = entry.path()?.to_path_buf();
74-
let unpacked = output_folder.join(&original_path);
75-
set_permission_mode(&unpacked, original_mode | 0o200)?;
82+
let safe_relpath = validate_entry_path(&original_path)?;
83+
let unpacked = output_folder.join(&safe_relpath);
84+
set_permission_mode(&unpacked, sanitize_archive_mode(original_mode) | 0o200)?;
7685

77-
read_only_dirs_and_modes.push((original_path, original_mode));
86+
read_only_dirs_and_modes.push((original_path, sanitize_archive_mode(original_mode)));
7887
}
7988
}
8089
_ => continue,
@@ -146,6 +155,7 @@ where
146155

147156
for explicit_path in explicit_paths {
148157
let previous_location = utils::cd_into_same_dir_as(explicit_path)?;
158+
let _cwd_guard = utils::CwdGuard::new(previous_location);
149159

150160
// Unwrap expectation:
151161
// paths should be canonicalized by now, and the root directory rejected.
@@ -231,7 +241,6 @@ where
231241
}
232242
}
233243
}
234-
env::set_current_dir(previous_location)?;
235244
}
236245

237246
Ok(builder.into_inner()?)

src/archive/zip.rs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#[cfg(unix)]
44
use std::os::unix::fs::PermissionsExt;
55
use std::{
6-
env,
76
io::{self, prelude::*},
87
path::{Path, PathBuf},
98
};
@@ -15,15 +14,18 @@ use same_file::Handle;
1514
use time::{OffsetDateTime, PrimitiveDateTime};
1615
use zip::{self, DateTime, ZipArchive, read::ZipFile};
1716

17+
#[cfg(unix)]
18+
use crate::utils::sanitize_archive_mode;
1819
use crate::{
1920
Result,
2021
error::FinalError,
2122
info, info_accessible,
2223
list::{FileInArchive, ListFileType},
2324
utils::{
24-
BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, cd_into_same_dir_as, create_symlink,
25-
ensure_parent_dir_exists, get_invalid_utf8_paths, is_same_file_as_output, pretty_format_list_of_paths,
26-
read_file_type, strip_cur_dir,
25+
BytesFmt, FileType, FileVisibilityPolicy, LimitedReader, PathFmt, SanitizedStr, canonicalize,
26+
cd_into_same_dir_as, create_symlink, ensure_parent_dir_exists, get_invalid_utf8_paths, is_same_file_as_output,
27+
max_decompressed_bytes, pretty_format_list_of_paths, read_file_type, strip_cur_dir, validate_dest_inside_root,
28+
validate_symlink_target,
2729
},
2830
warning,
2931
};
@@ -42,12 +44,17 @@ where
4244
Some(password) => archive.by_index_decrypt(idx, password)?,
4345
None => archive.by_index(idx)?,
4446
};
45-
let file_path = match file.enclosed_name() {
47+
let relpath = match file.enclosed_name() {
4648
Some(path) => path.to_owned(),
47-
None => continue,
49+
None => {
50+
warning!("skipping entry {} with unsafe name: {}", idx, SanitizedStr(file.name()));
51+
continue;
52+
}
4853
};
4954

50-
let file_path = output_folder.join(file_path);
55+
let file_path = output_folder.join(&relpath);
56+
57+
validate_dest_inside_root(output_folder, &file_path)?;
5158

5259
display_zip_comment_if_exists(&file);
5360

@@ -62,6 +69,7 @@ where
6269
let mut target = String::new();
6370
file.read_to_string(&mut target)?;
6471

72+
validate_symlink_target(&relpath, std::path::Path::new(&target))?;
6573
#[cfg(unix)]
6674
std::os::unix::fs::symlink(&target, &file_path)?;
6775
#[cfg(windows)]
@@ -81,12 +89,28 @@ where
8189
let mut target = String::new();
8290
file.read_to_string(&mut target)?;
8391

84-
info!("linking {} -> \"{}\"", PathFmt(file_path), target);
92+
validate_symlink_target(&relpath, std::path::Path::new(&target))?;
93+
info!("linking {} -> \"{}\"", PathFmt(file_path), SanitizedStr(&target));
8594

8695
create_symlink(Path::new(&target), file_path)?;
8796
} else {
97+
#[cfg(unix)]
98+
let mut output_file = {
99+
use fs_err::os::unix::fs::OpenOptionsExt;
100+
let mode = file.unix_mode().map(sanitize_archive_mode).unwrap_or(0o644);
101+
fs::OpenOptions::new()
102+
.write(true)
103+
.create(true)
104+
.truncate(true)
105+
.mode(mode)
106+
.open(file_path)?
107+
};
108+
#[cfg(not(unix))]
88109
let mut output_file = fs::File::create(file_path)?;
89-
io::copy(&mut file, &mut output_file)?;
110+
{
111+
let mut limited = LimitedReader::new(&mut file, max_decompressed_bytes());
112+
io::copy(&mut limited, &mut output_file)?;
113+
}
90114
set_last_modified_time(&file, file_path)?;
91115
#[cfg(unix)]
92116
unix_set_permissions(file_path, &file)?;
@@ -182,6 +206,7 @@ where
182206

183207
for explicit_path in input_filenames {
184208
let previous_location = cd_into_same_dir_as(explicit_path)?;
209+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
185210

186211
// Unwrap safety:
187212
// paths should be canonicalized by now, and the root directory rejected.
@@ -255,8 +280,6 @@ where
255280
}
256281
}
257282
}
258-
259-
env::set_current_dir(previous_location)?;
260283
}
261284

262285
let bytes = writer.finish()?;
@@ -276,7 +299,11 @@ fn display_zip_comment_if_exists<R: Read>(file: &ZipFile<'_, R>) {
276299
// the future, maybe asking the user if he wants to display the comment
277300
// (informing him of its size) would be sensible for both normal and
278301
// accessibility mode..
279-
info_accessible!("Found comment in {}: {}", file.name(), comment);
302+
info_accessible!(
303+
"Found comment in {}: {}",
304+
SanitizedStr(file.name()),
305+
SanitizedStr(comment)
306+
);
280307
}
281308
}
282309

@@ -315,7 +342,7 @@ fn unix_set_permissions<R: Read>(file_path: &Path, file: &ZipFile<'_, R>) -> Res
315342
use std::fs::Permissions;
316343

317344
if let Some(mode) = file.unix_mode() {
318-
fs::set_permissions(file_path, Permissions::from_mode(mode))?;
345+
fs::set_permissions(file_path, Permissions::from_mode(sanitize_archive_mode(mode)))?;
319346
}
320347

321348
Ok(())

src/cli/args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub struct CliArgs {
4343
pub format: Option<String>,
4444

4545
/// Decompress or list with password
46-
#[arg(short, long = "password", aliases = ["pass", "pw"], global = true)]
46+
#[arg(short, long = "password", aliases = ["pass", "pw"], env = "OUCH_PASSWORD", global = true)]
4747
pub password: Option<OsString>,
4848

4949
/// Concurrent working threads

0 commit comments

Comments
 (0)