diff --git a/crates/docpath/src/lib.rs b/crates/docpath/src/lib.rs index 7f6ca571..7509bf41 100644 --- a/crates/docpath/src/lib.rs +++ b/crates/docpath/src/lib.rs @@ -75,8 +75,9 @@ //! assert_eq!(resolve_absolute("img/logo.png", None), Err(Refused::Invalid)); //! ``` +use std::ffi::OsStr; use std::fmt; -use std::path::{MAIN_SEPARATOR, Path, PathBuf}; +use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; /// What a stored path refers to. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -619,18 +620,32 @@ fn resolve_native(raw: &str, doc: &Path, comps: &[String]) -> Result Result { - let mut p = doc.parent().map(Path::to_path_buf).unwrap_or_default(); - for c in comps { - match c.as_str() { - "." => {} - ".." => { - if !p.pop() { - return Err(Refused::Invalid); + let folder = doc.parent().unwrap_or_else(|| Path::new("")); + let link = comps.iter().map(|c| match c.as_str() { + "." => Component::CurDir, + ".." => Component::ParentDir, + name => Component::Normal(OsStr::new(name)), + }); + let mut root = PathBuf::new(); + let mut names: Vec<&OsStr> = Vec::new(); + for c in folder.components().chain(link) { + match c { + Component::Prefix(_) | Component::RootDir => root.push(c), + Component::CurDir => {} + Component::ParentDir => match names.last() { + Some(last) if *last != OsStr::new("..") => { + names.pop(); } - } - _ => p.push(c), + _ if root.has_root() => return Err(Refused::Invalid), + _ => names.push(c.as_os_str()), + }, + Component::Normal(name) => names.push(name), } } - Ok(p) + root.extend(names); + Ok(root) } diff --git a/crates/docpath/tests/docpath.rs b/crates/docpath/tests/docpath.rs index 6a2bf703..e7350cc2 100644 --- a/crates/docpath/tests/docpath.rs +++ b/crates/docpath/tests/docpath.rs @@ -425,6 +425,15 @@ fn a_non_unicode_document_path_keeps_the_namespace_rules() { assert_eq!(resolve("x.png", Some(folder)).unwrap(), Path::new(OsStr::from_bytes(b"/home/u/a\\b/x.png"))); let name = Path::new(OsStr::from_bytes(b"/home/u/a\xff\\b.doc")); assert_eq!(resolve("x.png", Some(name)), Err(Refused::Invalid)); + // `.` and `..` in the document's folder are folded as for a Unicode document path. + let dotted = Path::new(OsStr::from_bytes(b"/home/./x/../u/\xff/a.doc")); + assert_eq!(resolve("../y/x.png", Some(dotted)).unwrap(), Path::new("/home/u/y/x.png")); + assert_eq!(resolve("../../../../x.png", Some(dotted)), Err(Refused::Invalid)); + let relative = Path::new(OsStr::from_bytes(b"\xff/a.doc")); + assert_eq!(resolve("../../x.png", Some(relative)).unwrap(), Path::new("../x.png")); + // A `..` that climbs out of a name holding a `\` on Unix does not leave the share unnoticed. + let climb = Path::new(OsStr::from_bytes(b"//server/share/a\\b/../../\xff/a.doc")); + assert_eq!(resolve("x.png", Some(climb)), Err(Refused::Invalid)); } #[cfg(windows)] @@ -443,6 +452,10 @@ fn a_non_unicode_document_path_keeps_the_namespace_rules() { let share = native(r"\\server\share\docs\a.doc"); assert_eq!(resolve("x.png", Some(share.as_path())).unwrap(), native(r"\\server\share\docs\x.png")); assert_eq!(resolve(r"..\..\x.png", Some(share.as_path())), Err(Refused::Invalid)); + let dotted = native(r"C:\a\..\.\docs\a.doc"); + assert_eq!(resolve("x.png", Some(dotted.as_path())).unwrap(), native(r"C:\docs\x.png")); + assert_eq!(resolve(r"..\y\x.png", Some(dotted.as_path())).unwrap(), Path::new(r"C:\y\x.png")); + assert_eq!(resolve(r"..\..\x.png", Some(dotted.as_path())), Err(Refused::Invalid)); for doc in [r"\\.\COM1\docs\a.doc", r"\\?\GLOBALROOT\Device\docs\a.doc", r"\??\C:\docs\a.doc"] { assert_eq!(resolve("x.png", Some(native(doc).as_path())), Err(Refused::Device), "{doc}"); }