summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Turner <jturner.usa@gmail.com>2026-03-05 20:23:41 -0500
committerJohn Turner <jturner.usa@gmail.com>2026-03-05 20:23:58 -0500
commitfb2c9f640dbb493f7d8de16498bf673a82e019a4 (patch)
tree3f03216a68afe0b560df2c0e96c40fdb2c012d58
parentf3d6bceeffa043e7cbc626926ce75562d26fda78 (diff)
downloadhttpd-fb2c9f640dbb493f7d8de16498bf673a82e019a4.tar.gz
support traversing paths upwards but not past the server root
-rw-r--r--src/main.rs63
1 files changed, 54 insertions, 9 deletions
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<Response, HandleError> {
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<PathBuf, std::io::Error> {
- root.components()
- .chain(
- rest.components()
- .filter(|p| !matches!(p, Component::RootDir)),
- )
- .filter(|p| !matches!(p, Component::CurDir | Component::ParentDir))
- .collect::<PathBuf>()
- .canonicalize()
+fn join_path<A: AsRef<Path>, B: AsRef<Path>>(root: A, rest: B) -> Result<PathBuf, JoinPathError> {
+ 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)));
+ }
+}