Skip to content

Commit b589702

Browse files
author
curious-rabbit
committed
security improvements
1 parent 3b387dd commit b589702

11 files changed

Lines changed: 240 additions & 40 deletions

File tree

src/archive/sevenz.rs

Lines changed: 11 additions & 4 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
};
@@ -19,6 +18,7 @@ use crate::{
1918
list::{FileInArchive, ListFileType},
2019
utils::{
2120
BytesFmt, FileVisibilityPolicy, PathFmt, cd_into_same_dir_as, ensure_parent_dir_exists, is_same_file_as_output,
21+
validate_entry_path,
2222
},
2323
warning,
2424
};
@@ -33,7 +33,15 @@ where
3333
|entry: &ArchiveEntry, reader: &mut dyn Read, path: &PathBuf| -> Result<bool, sevenz_rust2::Error> {
3434
// Manually handle writing all files from 7z archive (the library defaults ignore empty files)
3535

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

3846
if entry.is_directory() {
3947
info!("File {} extracted to {}", entry.name(), PathFmt(&file_path));
@@ -136,6 +144,7 @@ where
136144

137145
for filename in files {
138146
let previous_location = cd_into_same_dir_as(filename)?;
147+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
139148

140149
// Unwrap safety:
141150
// paths should be canonicalized by now, and the root directory rejected.
@@ -172,8 +181,6 @@ where
172181

173182
writer.push_archive_entry::<fs::File>(entry, entry_data)?;
174183
}
175-
176-
env::set_current_dir(previous_location)?;
177184
}
178185

179186
let bytes = writer.finish()?;

src/archive/tar.rs

Lines changed: 18 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,7 @@ 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_entry_path, validate_symlink_target,
2423
},
2524
warning,
2625
};
@@ -38,22 +37,27 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path) -> Result<u64> {
3837

3938
match entry.header().entry_type() {
4039
tar::EntryType::Symlink => {
41-
let relative_path = entry.path()?;
42-
let full_path = output_folder.join(&relative_path);
40+
let raw_path = entry.path()?.into_owned();
41+
let safe_relpath = validate_entry_path(&raw_path)?;
42+
let full_path = output_folder.join(&safe_relpath);
4343
let target = entry
4444
.link_name()?
4545
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing symlink target"))?;
4646

47+
validate_symlink_target(&safe_relpath, &target)?;
4748
create_symlink(&target, &full_path)?;
4849
}
4950
tar::EntryType::Link => {
50-
let link_path = entry.path()?;
51-
let target = entry
51+
let raw_link = entry.path()?.into_owned();
52+
let safe_link_path = validate_entry_path(&raw_link)?;
53+
let raw_target = entry
5254
.link_name()?
53-
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing hardlink target"))?;
55+
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing hardlink target"))?
56+
.into_owned();
57+
let safe_target = validate_entry_path(&raw_target)?;
5458

55-
let full_link_path = output_folder.join(&link_path);
56-
let full_target_path = output_folder.join(&target);
59+
let full_link_path = output_folder.join(&safe_link_path);
60+
let full_target_path = output_folder.join(&safe_target);
5761

5862
fs::hard_link(&full_target_path, &full_link_path)?;
5963
}
@@ -71,10 +75,11 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path) -> Result<u64> {
7175
// We unpacked a read-only directory, make it writeable so that we can
7276
// create the files inside of it, by the end, restore the original mode
7377
let original_path = entry.path()?.to_path_buf();
74-
let unpacked = output_folder.join(&original_path);
75-
set_permission_mode(&unpacked, original_mode | 0o200)?;
78+
let safe_relpath = validate_entry_path(&original_path)?;
79+
let unpacked = output_folder.join(&safe_relpath);
80+
set_permission_mode(&unpacked, sanitize_archive_mode(original_mode) | 0o200)?;
7681

77-
read_only_dirs_and_modes.push((original_path, original_mode));
82+
read_only_dirs_and_modes.push((original_path, sanitize_archive_mode(original_mode)));
7883
}
7984
}
8085
_ => continue,
@@ -146,6 +151,7 @@ where
146151

147152
for explicit_path in explicit_paths {
148153
let previous_location = utils::cd_into_same_dir_as(explicit_path)?;
154+
let _cwd_guard = utils::CwdGuard::new(previous_location);
149155

150156
// Unwrap expectation:
151157
// paths should be canonicalized by now, and the root directory rejected.
@@ -231,7 +237,6 @@ where
231237
}
232238
}
233239
}
234-
env::set_current_dir(previous_location)?;
235240
}
236241

237242
Ok(builder.into_inner()?)

src/archive/zip.rs

Lines changed: 21 additions & 7 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,6 +14,8 @@ use same_file::Handle;
1514
use time::OffsetDateTime;
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,
@@ -23,7 +24,7 @@ use crate::{
2324
utils::{
2425
BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, cd_into_same_dir_as, create_symlink,
2526
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,
27+
read_file_type, strip_cur_dir, validate_symlink_target,
2728
},
2829
warning,
2930
};
@@ -42,12 +43,12 @@ where
4243
Some(password) => archive.by_index_decrypt(idx, password)?,
4344
None => archive.by_index(idx)?,
4445
};
45-
let file_path = match file.enclosed_name() {
46+
let relpath = match file.enclosed_name() {
4647
Some(path) => path.to_owned(),
4748
None => continue,
4849
};
4950

50-
let file_path = output_folder.join(file_path);
51+
let file_path = output_folder.join(&relpath);
5152

5253
display_zip_comment_if_exists(&file);
5354

@@ -62,6 +63,7 @@ where
6263
let mut target = String::new();
6364
file.read_to_string(&mut target)?;
6465

66+
validate_symlink_target(&relpath, std::path::Path::new(&target))?;
6567
#[cfg(unix)]
6668
std::os::unix::fs::symlink(&target, &file_path)?;
6769
#[cfg(windows)]
@@ -81,10 +83,23 @@ where
8183
let mut target = String::new();
8284
file.read_to_string(&mut target)?;
8385

86+
validate_symlink_target(&relpath, std::path::Path::new(&target))?;
8487
info!("linking {} -> \"{}\"", PathFmt(file_path), target);
8588

8689
create_symlink(Path::new(&target), file_path)?;
8790
} else {
91+
#[cfg(unix)]
92+
let mut output_file = {
93+
use fs_err::os::unix::fs::OpenOptionsExt;
94+
let mode = file.unix_mode().map(sanitize_archive_mode).unwrap_or(0o644);
95+
fs::OpenOptions::new()
96+
.write(true)
97+
.create(true)
98+
.truncate(true)
99+
.mode(mode)
100+
.open(file_path)?
101+
};
102+
#[cfg(not(unix))]
88103
let mut output_file = fs::File::create(file_path)?;
89104
io::copy(&mut file, &mut output_file)?;
90105
set_last_modified_time(&file, file_path)?;
@@ -182,6 +197,7 @@ where
182197

183198
for explicit_path in input_filenames {
184199
let previous_location = cd_into_same_dir_as(explicit_path)?;
200+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
185201

186202
// Unwrap safety:
187203
// paths should be canonicalized by now, and the root directory rejected.
@@ -255,8 +271,6 @@ where
255271
}
256272
}
257273
}
258-
259-
env::set_current_dir(previous_location)?;
260274
}
261275

262276
let bytes = writer.finish()?;
@@ -311,7 +325,7 @@ fn unix_set_permissions<R: Read>(file_path: &Path, file: &ZipFile<'_, R>) -> Res
311325
use std::fs::Permissions;
312326

313327
if let Some(mode) = file.unix_mode() {
314-
fs::set_permissions(file_path, Permissions::from_mode(mode))?;
328+
fs::set_permissions(file_path, Permissions::from_mode(sanitize_archive_mode(mode)))?;
315329
}
316330

317331
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

src/commands/decompress.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ use crate::{
1616
info, info_accessible,
1717
non_archive::lz4::MultiFrameLz4Decoder,
1818
utils::{
19-
self, BytesFmt, PathFmt, file_size,
19+
self, BytesFmt, LZMA_MEMLIMIT_BYTES, LimitedReader, PathFmt, file_size,
2020
io::{ReadSeek, lock_and_flush_output_stdio},
21-
is_path_stdin, resolve_path_conflict, user_wants_to_continue,
21+
is_path_stdin, max_decompressed_bytes, resolve_path_conflict, user_wants_to_continue,
2222
},
2323
};
2424

@@ -59,7 +59,11 @@ pub fn decompress_file(options: DecompressOptions) -> Result<()> {
5959
Box::new(bzip3::read::Bz3Decoder::new(decoder)?)
6060
}
6161
Lz4 => Box::new(MultiFrameLz4Decoder::new(decoder)),
62-
Lzma => Box::new(lzma_rust2::LzmaReader::new_mem_limit(decoder, u32::MAX, None)?),
62+
Lzma => Box::new(lzma_rust2::LzmaReader::new_mem_limit(
63+
decoder,
64+
LZMA_MEMLIMIT_BYTES,
65+
None,
66+
)?),
6367
Xz => Box::new(lzma_rust2::XzReader::new(decoder, true)),
6468
Lzip => Box::new(lzma_rust2::LzipReader::new(decoder)?),
6569
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
@@ -90,7 +94,9 @@ pub fn decompress_file(options: DecompressOptions) -> Result<()> {
9094
let control_flow = match first_extension {
9195
Gzip | Bzip | Bzip3 | Lz4 | Lzma | Xz | Lzip | Snappy | Zstd | Brotli => {
9296
let reader = create_decoder_up_to_first_extension()?;
93-
let mut reader = chain_reader_decoder(&first_extension, reader)?;
97+
let reader = chain_reader_decoder(&first_extension, reader)?;
98+
// Bomb cap: abort if decompressed output exceeds OUCH_MAX_DECOMPRESSED_BYTES
99+
let mut reader = LimitedReader::new(reader, max_decompressed_bytes());
94100

95101
let (mut writer, final_output_path) = match utils::create_file_or_prompt_on_conflict(
96102
&options.output_file_path,
@@ -142,7 +148,9 @@ pub fn decompress_file(options: DecompressOptions) -> Result<()> {
142148
drop(locks);
143149

144150
let mut vec = vec![];
145-
io::copy(&mut create_decoder_up_to_first_extension()?, &mut vec)?;
151+
// Bomb cap: abort if the in-memory decompressed image exceeds the limit
152+
let mut reader = LimitedReader::new(create_decoder_up_to_first_extension()?, max_decompressed_bytes());
153+
io::copy(&mut reader, &mut vec)?;
146154
Box::new(io::Cursor::new(vec))
147155
} else {
148156
Box::new(BufReader::with_capacity(

src/commands/list.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use crate::{
1111
extension::CompressionFormat::{self, *},
1212
list::{self, FileInArchive, ListOptions},
1313
non_archive::lz4::MultiFrameLz4Decoder,
14-
utils::{io::lock_and_flush_output_stdio, user_wants_to_continue},
14+
utils::{
15+
LZMA_MEMLIMIT_BYTES, LimitedReader, io::lock_and_flush_output_stdio, max_decompressed_bytes,
16+
user_wants_to_continue,
17+
},
1518
};
1619

1720
/// File at archive_path is opened for reading, example: "archive.tar.gz"
@@ -57,7 +60,11 @@ pub fn list_archive_contents(
5760
Box::new(bzip3::read::Bz3Decoder::new(decoder)?)
5861
}
5962
Lz4 => Box::new(MultiFrameLz4Decoder::new(decoder)),
60-
Lzma => Box::new(lzma_rust2::LzmaReader::new_mem_limit(decoder, u32::MAX, None)?),
63+
Lzma => Box::new(lzma_rust2::LzmaReader::new_mem_limit(
64+
decoder,
65+
LZMA_MEMLIMIT_BYTES,
66+
None,
67+
)?),
6168
Xz => Box::new(lzma_rust2::XzReader::new(decoder, true)),
6269
Lzip => Box::new(lzma_rust2::LzipReader::new(decoder)?),
6370
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
@@ -92,7 +99,9 @@ pub fn list_archive_contents(
9299
}
93100

94101
let mut vec = vec![];
95-
io::copy(&mut reader, &mut vec)?;
102+
// Bomb cap: abort if the in-memory decompressed image exceeds the limit
103+
let mut limited = LimitedReader::new(&mut reader, max_decompressed_bytes());
104+
io::copy(&mut limited, &mut vec)?;
96105
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
97106

98107
Box::new(crate::archive::zip::list_archive(zip_archive, password))
@@ -101,7 +110,9 @@ pub fn list_archive_contents(
101110
Rar => {
102111
if formats.len() > 1 {
103112
let mut temp_file = tempfile::NamedTempFile::new()?;
104-
io::copy(&mut reader, &mut temp_file)?;
113+
// Bomb cap: abort if decompressed output to tempfile exceeds the limit
114+
let mut limited = LimitedReader::new(&mut reader, max_decompressed_bytes());
115+
io::copy(&mut limited, &mut temp_file)?;
105116
Box::new(crate::archive::rar::list_archive(temp_file.path(), password)?)
106117
} else {
107118
Box::new(crate::archive::rar::list_archive(archive_path, password)?)
@@ -122,7 +133,9 @@ pub fn list_archive_contents(
122133
drop(locks);
123134

124135
let mut vec = vec![];
125-
io::copy(&mut reader, &mut vec)?;
136+
// Bomb cap: abort if the in-memory decompressed image exceeds the limit
137+
let mut limited = LimitedReader::new(&mut reader, max_decompressed_bytes());
138+
io::copy(&mut limited, &mut vec)?;
126139

127140
Box::new(archive::sevenz::list_archive(io::Cursor::new(vec), password)?)
128141
} else {

0 commit comments

Comments
 (0)