From fb2c9f640dbb493f7d8de16498bf673a82e019a4 Mon Sep 17 00:00:00 2001 From: John Turner Date: Thu, 5 Mar 2026 20:23:41 -0500 Subject: support traversing paths upwards but not past the server root --- src/main.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index b422f47..401af02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,6 +78,15 @@ enum InitLuaError { EvalConfig(mlua::Error), } +#[derive(Debug, thiserror::Error)] +enum JoinPathError { + #[error("unsafe path")] + Unsafe, + + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + async fn handle(handlers: Table, request: Request) -> Result { let method = request.method().to_string(); @@ -128,15 +137,36 @@ async fn response(handlers: Table, stream: TcpStream) -> Result<(), ResponseErro Ok(()) } -fn join_path(root: &Path, rest: &Path) -> Result { - root.components() - .chain( - rest.components() - .filter(|p| !matches!(p, Component::RootDir)), - ) - .filter(|p| !matches!(p, Component::CurDir | Component::ParentDir)) - .collect::() - .canonicalize() +fn join_path, B: AsRef>(root: A, rest: B) -> Result { + let mut pathbuf = root.as_ref().to_path_buf(); + + for component in rest.as_ref().components() { + match component { + Component::Normal(path) => { + pathbuf.push(path); + } + Component::ParentDir => { + pathbuf.pop(); + + if !pathbuf.starts_with(root.as_ref()) { + return Err(JoinPathError::Unsafe); + } + } + _ => continue, + } + } + + let canon = match pathbuf.canonicalize() { + Ok(canon) => canon, + Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => pathbuf, + Err(e) => return Err(JoinPathError::Io(e)), + }; + + if !canon.starts_with(root.as_ref()) { + return Err(JoinPathError::Unsafe); + } + + Ok(canon) } fn init_lua(lua: Lua) -> Result<(), InitLuaError> { @@ -214,3 +244,18 @@ async fn main() -> ExitCode { }); } } + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_path_traversal() { + let root = "/var/www"; + let get = "/../../bin/sh"; + let joined = join_path(root, get); + + assert!(matches!(joined, Err(JoinPathError::Unsafe))); + } +} -- cgit v1.2.3