Skip to content

Commit 313c546

Browse files
aweinstock314aweinstock
andauthored
uucore: Disallow slashes in determine_backup_suffix (#11149)
* uucore: Disallow slashes in determine_backup_suffix; use ln's OsStr-compatible implementations of *_backup_path. * ln: Add test for backup suffix containing slashes. --------- Co-authored-by: aweinstock <avi@zellic.io>
1 parent 2a81c11 commit 313c546

3 files changed

Lines changed: 55 additions & 55 deletions

File tree

src/uu/ln/src/ln.rs

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,7 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> {
383383
};
384384

385385
if dst.is_symlink() || dst.exists() {
386-
backup_path = match settings.backup {
387-
BackupMode::None => None,
388-
BackupMode::Simple => Some(simple_backup_path(dst, &settings.suffix)),
389-
BackupMode::Numbered => Some(numbered_backup_path(dst)),
390-
BackupMode::Existing => Some(existing_backup_path(dst, &settings.suffix)),
391-
};
386+
backup_path = backup_control::get_backup_path(settings.backup, dst, &settings.suffix);
392387
if settings.backup == BackupMode::Existing && !settings.symbolic {
393388
// when ln --backup f f, it should detect that it is the same file
394389
if paths_refer_to_same_file(src, dst, true) {
@@ -463,31 +458,6 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> {
463458
Ok(())
464459
}
465460

466-
fn simple_backup_path(path: &Path, suffix: &OsString) -> PathBuf {
467-
let mut file_name = path.file_name().unwrap_or_default().to_os_string();
468-
file_name.push(suffix);
469-
path.with_file_name(file_name)
470-
}
471-
472-
fn numbered_backup_path(path: &Path) -> PathBuf {
473-
let mut i: u64 = 1;
474-
loop {
475-
let new_path = simple_backup_path(path, &OsString::from(format!(".~{i}~")));
476-
if !new_path.exists() {
477-
return new_path;
478-
}
479-
i += 1;
480-
}
481-
}
482-
483-
fn existing_backup_path(path: &Path, suffix: &OsString) -> PathBuf {
484-
let test_path = simple_backup_path(path, &OsString::from(".~1~"));
485-
if test_path.exists() {
486-
return numbered_backup_path(path);
487-
}
488-
simple_backup_path(path, suffix)
489-
}
490-
491461
#[cfg(windows)]
492462
pub fn symlink<P1: AsRef<Path>, P2: AsRef<Path>>(src: P1, dst: P2) -> std::io::Result<()> {
493463
if src.as_ref().is_dir() {

src/uucore/src/lib/features/backup_control.rs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ use clap::ArgMatches;
8989
use std::{
9090
env,
9191
error::Error,
92+
ffi::{OsStr, OsString},
9293
fmt::{Debug, Display},
9394
path::{Path, PathBuf},
9495
};
@@ -243,16 +244,21 @@ pub mod arguments {
243244
///
244245
/// 1. From the '-S' or '--suffix' CLI argument, if present
245246
/// 2. From the "SIMPLE_BACKUP_SUFFIX" environment variable, if present
246-
/// 3. By using the default '~' if none of the others apply
247+
/// 3. By using the default '~' if none of the others apply, or if they contained slashes
247248
///
248249
/// This function directly takes [`ArgMatches`] as argument and looks for
249250
/// the '-S' and '--suffix' arguments itself.
250251
pub fn determine_backup_suffix(matches: &ArgMatches) -> String {
251252
let supplied_suffix = matches.get_one::<String>(arguments::OPT_SUFFIX);
252-
if let Some(suffix) = supplied_suffix {
253+
let suffix = if let Some(suffix) = supplied_suffix {
253254
String::from(suffix)
254255
} else {
255256
env::var("SIMPLE_BACKUP_SUFFIX").unwrap_or_else(|_| DEFAULT_BACKUP_SUFFIX.to_owned())
257+
};
258+
if suffix.contains('/') {
259+
DEFAULT_BACKUP_SUFFIX.to_owned()
260+
} else {
261+
suffix
256262
}
257263
}
258264

@@ -413,48 +419,42 @@ fn match_method(method: &str, origin: &str) -> UResult<BackupMode> {
413419
}
414420
}
415421

416-
pub fn get_backup_path(
422+
pub fn get_backup_path<S: AsRef<OsStr>>(
417423
backup_mode: BackupMode,
418424
backup_path: &Path,
419-
suffix: &str,
425+
suffix: S,
420426
) -> Option<PathBuf> {
421427
match backup_mode {
422428
BackupMode::None => None,
423-
BackupMode::Simple => Some(simple_backup_path(backup_path, suffix)),
429+
BackupMode::Simple => Some(simple_backup_path(backup_path, suffix.as_ref())),
424430
BackupMode::Numbered => Some(numbered_backup_path(backup_path)),
425-
BackupMode::Existing => Some(existing_backup_path(backup_path, suffix)),
431+
BackupMode::Existing => Some(existing_backup_path(backup_path, suffix.as_ref())),
426432
}
427433
}
428434

429-
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
435+
fn simple_backup_path<S: AsRef<OsStr>>(path: &Path, suffix: S) -> PathBuf {
430436
let mut file_name = path.file_name().unwrap_or_default().to_os_string();
431-
file_name.push(suffix);
437+
file_name.push(suffix.as_ref());
432438
path.with_file_name(file_name)
433439
}
434440

435441
fn numbered_backup_path(path: &Path) -> PathBuf {
436-
let file_name = path.file_name().unwrap_or_default();
437-
for i in 1_u64.. {
438-
let mut numbered_file_name = file_name.to_os_string();
439-
numbered_file_name.push(format!(".~{i}~"));
440-
let path = path.with_file_name(numbered_file_name);
441-
if !path.exists() {
442-
return path;
442+
let mut i: u64 = 1;
443+
loop {
444+
let new_path = simple_backup_path(path, OsString::from(format!(".~{i}~")));
445+
if !new_path.exists() {
446+
return new_path;
443447
}
448+
i += 1;
444449
}
445-
panic!("cannot create backup")
446450
}
447451

448-
fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
449-
let file_name = path.file_name().unwrap_or_default();
450-
let mut numbered_file_name = file_name.to_os_string();
451-
numbered_file_name.push(".~1~");
452-
let test_path = path.with_file_name(numbered_file_name);
452+
fn existing_backup_path<S: AsRef<OsStr>>(path: &Path, suffix: S) -> PathBuf {
453+
let test_path = simple_backup_path(path, OsString::from(".~1~"));
453454
if test_path.exists() {
454-
numbered_backup_path(path)
455-
} else {
456-
simple_backup_path(path, suffix)
455+
return numbered_backup_path(path);
457456
}
457+
simple_backup_path(path, suffix.as_ref())
458458
}
459459

460460
/// Returns true if the source file is likely to be the simple backup file for the target file.
@@ -695,6 +695,16 @@ mod tests {
695695
assert_eq!(result, "-v");
696696
}
697697

698+
#[test]
699+
fn test_suffix_rejects_path_traversal() {
700+
let _dummy = TEST_MUTEX.lock().unwrap();
701+
let matches =
702+
make_app().get_matches_from(vec!["command", "-b", "--suffix", "_/../../dest"]);
703+
704+
let result = determine_backup_suffix(&matches);
705+
assert_eq!(result, DEFAULT_BACKUP_SUFFIX);
706+
}
707+
698708
#[test]
699709
fn test_numbered_backup_path() {
700710
assert_eq!(numbered_backup_path(Path::new("")), PathBuf::from(".~1~"));

tests/by-util/test_ln.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,3 +1023,23 @@ fn test_ln_hard_link_dir() {
10231023
.fails()
10241024
.stderr_contains("hard link not allowed for directory");
10251025
}
1026+
1027+
#[test]
1028+
fn test_ln_backup_no_path_traversal() {
1029+
let scene = TestScenario::new(util_name!());
1030+
let at = &scene.fixtures;
1031+
1032+
at.touch("a");
1033+
at.touch("b");
1034+
at.mkdir("b_");
1035+
1036+
scene
1037+
.ucmd()
1038+
.args(&["-S", "_/../c", "-s", "a", "b"])
1039+
.succeeds();
1040+
1041+
assert!(!at.file_exists("c"));
1042+
assert!(at.plus("b").is_symlink());
1043+
assert!(at.file_exists("b~"));
1044+
assert!(!at.plus("b~").is_symlink());
1045+
}

0 commit comments

Comments
 (0)