Use Absolutize for files that do not exist

Canonicalize relies on the file existing to resolve the potential
traversal, which won't work for file upload in case the file name has a
traversal inside it.
This commit is contained in:
Cameron Cordes
2020-10-17 19:22:55 -04:00
parent 6c9c80f61d
commit eccb45ced0
4 changed files with 29 additions and 16 deletions

19
Cargo.lock generated
View File

@@ -1133,6 +1133,7 @@ dependencies = [
"image", "image",
"jsonwebtoken", "jsonwebtoken",
"notify", "notify",
"path-absolutize",
"rayon", "rayon",
"serde", "serde",
"serde_json", "serde_json",
@@ -1596,6 +1597,24 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "path-absolutize"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6ab2aaa5faefed84db46e4398eab15fa51325606462b5da8b0e230af3ac59a"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658c6e985fce9c25289fe7c86c08a3cbe82c19a3cd5b3bc5945c8c632552e460"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "pem" name = "pem"
version = "0.8.1" version = "0.8.1"

View File

@@ -26,3 +26,4 @@ walkdir = "2"
rayon = "1.3" rayon = "1.3"
notify = "4.0" notify = "4.0"
tokio = "0.2" tokio = "0.2"
path-absolutize = "3.0.6"

View File

@@ -1,3 +1,4 @@
use path_absolutize::*;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs::read_dir; use std::fs::read_dir;
use std::io; use std::io;
@@ -47,10 +48,10 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
let mut full_path = PathBuf::from(base); let mut full_path = PathBuf::from(base);
full_path.push(&path); full_path.push(&path);
full_path full_path
.canonicalize() .absolutize()
.and_then(|p| { .and_then(|p| {
if p.starts_with(base) { if p.starts_with(base) {
Ok(p) Ok(p.into_owned())
} else { } else {
Err(io::Error::new( Err(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
@@ -59,9 +60,9 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option<PathBuf> {
} }
}) })
.ok() .ok()
} else if let Ok(path) = path.canonicalize().and_then(|path| { } else if let Ok(path) = path.absolutize().and_then(|path| {
if path.starts_with(base) { if path.starts_with(base) {
Ok(path) Ok(path.into_owned())
} else { } else {
Err(io::Error::new( Err(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
@@ -88,9 +89,7 @@ mod tests {
assert_eq!(None, is_valid_path("fake/../../../")); assert_eq!(None, is_valid_path("fake/../../../"));
assert_eq!(None, is_valid_path("../../../etc/passwd")); assert_eq!(None, is_valid_path("../../../etc/passwd"));
assert_eq!(None, is_valid_path("..//etc/passwd")); assert_eq!(None, is_valid_path("..//etc/passwd"));
assert_eq!(None, is_valid_path("..%2f..%2f/etc/passwd")); assert_eq!(None, is_valid_path("../../etc/passwd"));
assert_eq!(None, is_valid_path("%2e%2e%2f%2e%2e%2f/etc/passwd"));
assert_eq!(None, is_valid_path("\\\\attacker.com\\shared\\mal.php"));
} }
#[test] #[test]

View File

@@ -153,13 +153,8 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
let path = file_path.unwrap_or_else(|| dotenv::var("BASE_PATH").unwrap()); let path = file_path.unwrap_or_else(|| dotenv::var("BASE_PATH").unwrap());
if !file_content.is_empty() { if !file_content.is_empty() {
let full_path = PathBuf::from(&path); let full_path = PathBuf::from(&path).join(file_name.unwrap());
if let Some(full_path) = is_valid_path(full_path.to_str().unwrap_or("")) {
if let Some(mut full_path) = is_valid_path(full_path.to_str().unwrap_or("")) {
// TODO: Validate this file_name as is subject to path traversals which could lead to
// writing outside the base dir.
full_path = full_path.join(file_name.unwrap());
if !full_path.is_file() { if !full_path.is_file() {
let mut file = File::create(full_path).unwrap(); let mut file = File::create(full_path).unwrap();
file.write_all(&file_content).unwrap(); file.write_all(&file_content).unwrap();
@@ -206,8 +201,7 @@ async fn stream_video(
// Extract video playlist dir to dotenv // Extract video playlist dir to dotenv
if !playlist.starts_with("tmp") || playlist.contains("..") { if !playlist.starts_with("tmp") || playlist.contains("..") {
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()
} } else if let Ok(file) = NamedFile::open(playlist) {
else if let Ok(file) = NamedFile::open(playlist) {
file.into_response(&request).unwrap() file.into_response(&request).unwrap()
} else { } else {
HttpResponse::NotFound().finish() HttpResponse::NotFound().finish()