mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
🔒 Fix potential symlink bypass in path_allowed
Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
bede889ced
commit
98e9a7b23a
5 changed files with 128 additions and 2 deletions
BIN
poc
Executable file
BIN
poc
Executable file
Binary file not shown.
57
poc.rs
Normal file
57
poc.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn canon_or_raw(p: &Path) -> PathBuf {
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for component in p.components() {
|
||||||
|
match component {
|
||||||
|
std::path::Component::ParentDir => {
|
||||||
|
let can_pop = matches!(
|
||||||
|
normalized.components().next_back(),
|
||||||
|
Some(std::path::Component::Normal(_))
|
||||||
|
);
|
||||||
|
if can_pop {
|
||||||
|
normalized.pop();
|
||||||
|
} else if !normalized.has_root() {
|
||||||
|
normalized.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::path::Component::CurDir => {}
|
||||||
|
_ => normalized.push(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secure_canonicalize(path: &Path) -> PathBuf {
|
||||||
|
let mut p = PathBuf::new();
|
||||||
|
for component in path.components() {
|
||||||
|
p.push(component);
|
||||||
|
if let Ok(canon) = fs::canonicalize(&p) {
|
||||||
|
p = canon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canon_or_raw(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let temp = std::env::temp_dir();
|
||||||
|
let real_allowed = temp.join("allowed_dir_abc");
|
||||||
|
fs::create_dir_all(&real_allowed).unwrap();
|
||||||
|
let secret = temp.join("secret_dir_xyz");
|
||||||
|
fs::create_dir_all(&secret).unwrap();
|
||||||
|
|
||||||
|
// Attacker makes a symlink from allowed/sym -> target
|
||||||
|
let symlink = real_allowed.join("sym");
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::os::unix::fs::symlink(&secret, &symlink).unwrap_or(());
|
||||||
|
|
||||||
|
// Attacker tries to write to allowed/sym/../secret_dir_xyz/new_file
|
||||||
|
let malicious = symlink.join("..").join("secret_dir_xyz").join("new_file");
|
||||||
|
println!("malicious path: {:?}", malicious);
|
||||||
|
println!("secure_canonicalize: {:?}", secure_canonicalize(&malicious));
|
||||||
|
println!("canon_or_raw: {:?}", canon_or_raw(&malicious));
|
||||||
|
|
||||||
|
let allowed = real_allowed.clone();
|
||||||
|
println!("secure_canonicalize(allowed): {:?}", secure_canonicalize(&allowed));
|
||||||
|
}
|
||||||
|
|
@ -67,12 +67,23 @@ fn canon_or_raw(p: &Path) -> PathBuf {
|
||||||
normalized
|
normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn secure_canonicalize(path: &Path) -> PathBuf {
|
||||||
|
let mut p = PathBuf::new();
|
||||||
|
for component in path.components() {
|
||||||
|
p.push(component);
|
||||||
|
if let Ok(canon) = fs::canonicalize(&p) {
|
||||||
|
p = canon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canon_or_raw(&p)
|
||||||
|
}
|
||||||
|
|
||||||
/// Exact match against an authorized folder, compared canonicalized so a
|
/// Exact match against an authorized folder, compared canonicalized so a
|
||||||
/// symlink or ".." spelling cannot stand in for a different path. A subfolder
|
/// symlink or ".." spelling cannot stand in for a different path. A subfolder
|
||||||
/// of an authorized folder is not itself authorized.
|
/// of an authorized folder is not itself authorized.
|
||||||
fn path_allowed(candidate: &Path, allowed: &[PathBuf]) -> bool {
|
fn path_allowed(candidate: &Path, allowed: &[PathBuf]) -> bool {
|
||||||
let c = canon_or_raw(candidate);
|
let c = secure_canonicalize(candidate);
|
||||||
allowed.iter().any(|a| canon_or_raw(a) == c)
|
allowed.iter().any(|a| secure_canonicalize(a) == c)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The custom folder recorded in the app's own settings.json, if any. Only
|
/// The custom folder recorded in the app's own settings.json, if any. Only
|
||||||
|
|
|
||||||
BIN
test_fix
Executable file
BIN
test_fix
Executable file
Binary file not shown.
58
test_fix.rs
Normal file
58
test_fix.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn canon_or_raw(p: &Path) -> PathBuf {
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for component in p.components() {
|
||||||
|
match component {
|
||||||
|
std::path::Component::ParentDir => {
|
||||||
|
let can_pop = matches!(
|
||||||
|
normalized.components().next_back(),
|
||||||
|
Some(std::path::Component::Normal(_))
|
||||||
|
);
|
||||||
|
if can_pop {
|
||||||
|
normalized.pop();
|
||||||
|
} else if !normalized.has_root() {
|
||||||
|
normalized.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::path::Component::CurDir => {}
|
||||||
|
_ => normalized.push(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secure_canonicalize(path: &Path) -> PathBuf {
|
||||||
|
let mut p = PathBuf::new();
|
||||||
|
for component in path.components() {
|
||||||
|
p.push(component);
|
||||||
|
if let Ok(canon) = fs::canonicalize(&p) {
|
||||||
|
p = canon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We still need to run canon_or_raw because non-existent parts
|
||||||
|
// (e.g. trailing ".." or ".") were pushed but not canonicalized by fs::canonicalize
|
||||||
|
canon_or_raw(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_allowed(candidate: &Path, allowed: &[PathBuf]) -> bool {
|
||||||
|
let c = secure_canonicalize(candidate);
|
||||||
|
allowed.iter().any(|a| secure_canonicalize(a) == c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let temp = std::env::temp_dir();
|
||||||
|
let real_allowed = temp.join("allowed_dir_abc");
|
||||||
|
fs::create_dir_all(&real_allowed).unwrap();
|
||||||
|
let secret = temp.join("secret_dir_xyz");
|
||||||
|
fs::create_dir_all(&secret).unwrap();
|
||||||
|
|
||||||
|
// Attacker makes a symlink from allowed/sym -> target
|
||||||
|
let symlink = real_allowed.join("sym");
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::os::unix::fs::symlink(&secret, &symlink).unwrap_or(());
|
||||||
|
|
||||||
|
let malicious = symlink.join("..").join("secret_dir_xyz").join("new_file");
|
||||||
|
println!("Does malicious match? {}", path_allowed(&malicious, &[real_allowed.clone()]));
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue