Skip to content

Commit 25eb2cf

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

14 files changed

Lines changed: 269 additions & 53 deletions

File tree

src/archive/rar.rs

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

1414
/// Unpacks the archive given by `archive_path` into the folder given by `output_folder`.
@@ -28,7 +28,7 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio
2828
info!(
2929
"extracted ({}) {}",
3030
BytesFmt(entry.unpacked_size),
31-
entry.filename.display(),
31+
PathFmt(&entry.filename),
3232
);
3333
files_unpacked += 1;
3434
header.extract_with_base(output_folder)?

src/archive/sevenz.rs

Lines changed: 17 additions & 6 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,8 @@ 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, PathFmt, SanitizedStr, cd_into_same_dir_as, ensure_parent_dir_exists,
21+
is_same_file_as_output, validate_entry_path,
2222
},
2323
warning,
2424
};
@@ -33,10 +33,22 @@ 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() {
39-
info!("File {} extracted to {}", entry.name(), PathFmt(&file_path));
47+
info!(
48+
"File {} extracted to {}",
49+
SanitizedStr(entry.name()),
50+
PathFmt(&file_path)
51+
);
4052
if !path.fs_err_try_exists()? {
4153
fs::create_dir_all(path)?;
4254
}
@@ -136,6 +148,7 @@ where
136148

137149
for filename in files {
138150
let previous_location = cd_into_same_dir_as(filename)?;
151+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
139152

140153
// Unwrap safety:
141154
// paths should be canonicalized by now, and the root directory rejected.
@@ -172,8 +185,6 @@ where
172185

173186
writer.push_archive_entry::<fs::File>(entry, entry_data)?;
174187
}
175-
176-
env::set_current_dir(previous_location)?;
177188
}
178189

179190
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: 33 additions & 12 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,17 @@ 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,
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, PathFmt, SanitizedStr, canonicalize, cd_into_same_dir_as,
26+
create_symlink, ensure_parent_dir_exists, get_invalid_utf8_paths, is_same_file_as_output,
27+
pretty_format_list_of_paths, read_file_type, strip_cur_dir, validate_symlink_target,
2728
},
2829
warning,
2930
};
@@ -42,12 +43,15 @@ 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(),
47-
None => continue,
48+
None => {
49+
warning!("skipping entry {} with unsafe name: {}", idx, SanitizedStr(file.name()));
50+
continue;
51+
}
4852
};
4953

50-
let file_path = output_folder.join(file_path);
54+
let file_path = output_folder.join(&relpath);
5155

5256
display_zip_comment_if_exists(&file);
5357

@@ -62,6 +66,7 @@ where
6266
let mut target = String::new();
6367
file.read_to_string(&mut target)?;
6468

69+
validate_symlink_target(&relpath, std::path::Path::new(&target))?;
6570
#[cfg(unix)]
6671
std::os::unix::fs::symlink(&target, &file_path)?;
6772
#[cfg(windows)]
@@ -81,10 +86,23 @@ where
8186
let mut target = String::new();
8287
file.read_to_string(&mut target)?;
8388

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

8692
create_symlink(Path::new(&target), file_path)?;
8793
} else {
94+
#[cfg(unix)]
95+
let mut output_file = {
96+
use fs_err::os::unix::fs::OpenOptionsExt;
97+
let mode = file.unix_mode().map(sanitize_archive_mode).unwrap_or(0o644);
98+
fs::OpenOptions::new()
99+
.write(true)
100+
.create(true)
101+
.truncate(true)
102+
.mode(mode)
103+
.open(file_path)?
104+
};
105+
#[cfg(not(unix))]
88106
let mut output_file = fs::File::create(file_path)?;
89107
io::copy(&mut file, &mut output_file)?;
90108
set_last_modified_time(&file, file_path)?;
@@ -182,6 +200,7 @@ where
182200

183201
for explicit_path in input_filenames {
184202
let previous_location = cd_into_same_dir_as(explicit_path)?;
203+
let _cwd_guard = crate::utils::CwdGuard::new(previous_location);
185204

186205
// Unwrap safety:
187206
// paths should be canonicalized by now, and the root directory rejected.
@@ -255,8 +274,6 @@ where
255274
}
256275
}
257276
}
258-
259-
env::set_current_dir(previous_location)?;
260277
}
261278

262279
let bytes = writer.finish()?;
@@ -276,7 +293,11 @@ fn display_zip_comment_if_exists<R: Read>(file: &ZipFile<'_, R>) {
276293
// the future, maybe asking the user if he wants to display the comment
277294
// (informing him of its size) would be sensible for both normal and
278295
// accessibility mode..
279-
info_accessible!("Found comment in {}: {}", file.name(), comment);
296+
info_accessible!(
297+
"Found comment in {}: {}",
298+
SanitizedStr(file.name()),
299+
SanitizedStr(comment)
300+
);
280301
}
281302
}
282303

@@ -311,7 +332,7 @@ fn unix_set_permissions<R: Read>(file_path: &Path, file: &ZipFile<'_, R>) -> Res
311332
use std::fs::Permissions;
312333

313334
if let Some(mode) = file.unix_mode() {
314-
fs::set_permissions(file_path, Permissions::from_mode(mode))?;
335+
fs::set_permissions(file_path, Permissions::from_mode(sanitize_archive_mode(mode)))?;
315336
}
316337

317338
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)