From 80e94329fa9c60f2e7c12ef8b8e4c3f417953a39 Mon Sep 17 00:00:00 2001 From: Eduardo Trujillo <ed@chromabits.com> Date: Mon, 14 Nov 2022 23:08:06 -0800 Subject: [PATCH] feat(files): Add support for percent-encoding in paths --- src/files/pathbuf.rs | 116 ++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/src/files/pathbuf.rs b/src/files/pathbuf.rs index a336a5e..429834e 100644 --- a/src/files/pathbuf.rs +++ b/src/files/pathbuf.rs @@ -4,11 +4,13 @@ use actix_web::{ FromRequest, HttpRequest, ResponseError, }; use futures_util::future::{ready, Ready}; +use percent_encoding::percent_decode_str; use snafu::Snafu; use std::{ convert::{TryFrom, TryInto}, ops::Deref, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, + str::FromStr, }; #[derive(Snafu, Debug, PartialEq)] @@ -22,6 +24,12 @@ pub enum Error { /// The segment ended with the wrapped invalid character. #[snafu(display("The segment ended with the wrapped invalid character."))] BadEnd { char: char }, + /// The path is not a valid UTF-8 string after percent-decoding + #[snafu(display("The path is not a valid UTF-8 string after percent-decoding."))] + NotValidUtf8, + /// The path has invalid or unexpected components. + #[snafu(display("The path has invalid or unexpected components."))] + InvalidComponents, } /// Return `BadRequest` for `Error`. @@ -37,12 +45,23 @@ type Result<T, E = Error> = std::result::Result<T, E>; pub struct UriPathBuf(PathBuf); impl UriPathBuf { - pub fn new(path: &str) -> Result<Self> { - let mut buf = PathBuf::new(); + pub fn new(raw_path: &str) -> Result<Self> { + let mut path = PathBuf::new(); - for segment in path.split('/') { + let mut segment_count = raw_path.matches('/').count() + 1; + + let decoded_path = percent_decode_str(raw_path) + .decode_utf8() + .map_err(|_| Error::NotValidUtf8)?; + + if segment_count != decoded_path.matches('/').count() + 1 { + return Err(Error::BadChar { char: '/' }); + } + + for segment in decoded_path.split('/') { if segment == ".." { - buf.pop(); + segment_count -= 1; + path.pop(); } else if segment.starts_with('.') { return Err(Error::BadStart { char: '.' }); } else if segment.starts_with('*') { @@ -54,15 +73,32 @@ impl UriPathBuf { } else if segment.ends_with('<') { return Err(Error::BadEnd { char: '<' }); } else if segment.is_empty() { + segment_count -= 1; continue; } else if cfg!(windows) && segment.contains('\\') { return Err(Error::BadChar { char: '\\' }); + } else if cfg!(windows) && segment.contains(':') { + return Err(Error::BadChar { char: ':' }); } else { - buf.push(segment) + path.push(segment) + } + } + + for (i, component) in path.components().enumerate() { + if !matches!(component, Component::Normal(_)) || i >= segment_count { + return Err(Error::InvalidComponents); } } - Ok(UriPathBuf(buf)) + Ok(UriPathBuf(path)) + } +} + +impl FromStr for UriPathBuf { + type Err = Error; + + fn from_str(raw_path: &str) -> Result<Self, Self::Err> { + Self::new(raw_path) } } @@ -70,8 +106,8 @@ impl FromRequest for UriPathBuf { type Error = Error; type Future = Ready<Result<Self, Self::Error>>; - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(req.try_into()) + fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(request.try_into()) } } @@ -79,7 +115,7 @@ impl TryFrom<&HttpRequest> for UriPathBuf { type Error = Error; fn try_from(request: &HttpRequest) -> Result<Self, Error> { - UriPathBuf::new(request.match_info().path()) + request.match_info().unprocessed().parse() } } @@ -87,13 +123,13 @@ impl TryFrom<&ServiceRequest> for UriPathBuf { type Error = Error; fn try_from(request: &ServiceRequest) -> Result<Self, Error> { - UriPathBuf::new(request.match_info().path()) + request.match_info().unprocessed().parse() } } impl From<UriPathBuf> for PathBuf { - fn from(buf: UriPathBuf) -> PathBuf { - buf.0 + fn from(path: UriPathBuf) -> PathBuf { + path.0 } } @@ -110,33 +146,31 @@ mod tests { #[actix_rt::test] async fn test_path_buf() { - assert_eq!( - UriPathBuf::new("/test/.tt").map(|t| t.0), - Err(Error::BadStart { char: '.' }) - ); - assert_eq!( - UriPathBuf::new("/test/*tt").map(|t| t.0), - Err(Error::BadStart { char: '*' }) - ); - assert_eq!( - UriPathBuf::new("/test/tt:").map(|t| t.0), - Err(Error::BadEnd { char: ':' }) - ); - assert_eq!( - UriPathBuf::new("/test/tt<").map(|t| t.0), - Err(Error::BadEnd { char: '<' }) - ); - assert_eq!( - UriPathBuf::new("/test/tt>").map(|t| t.0), - Err(Error::BadEnd { char: '>' }) - ); - assert_eq!( - UriPathBuf::new("/seg1/seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg1", "seg2"]) - ); - assert_eq!( - UriPathBuf::new("/seg1/../seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg2"]) - ); + let cases: &[(&str, Result<PathBuf, Error>)] = &[ + ("/test/.tt", Err(Error::BadStart { char: '.' })), + ("/test/*tt", Err(Error::BadStart { char: '*' })), + ("/test/tt:", Err(Error::BadEnd { char: ':' })), + ("/test/tt<", Err(Error::BadEnd { char: '<' })), + ("/test/tt>", Err(Error::BadEnd { char: '>' })), + ("hello%20world", Ok(PathBuf::from_iter(vec!["hello world"]))), + ( + "/testing%21/hello%20world", + Ok(PathBuf::from_iter(vec!["testing!", "hello world"])), + ), + ("/seg1/seg2/", Ok(PathBuf::from_iter(vec!["seg1", "seg2"]))), + ("/seg1/../seg2/", Ok(PathBuf::from_iter(vec!["seg2"]))), + ( + "/../../../../../dev/null", + Ok(PathBuf::from_iter(vec!["dev/null"])), + ), + ( + "/../../../..%2F../dev/null", + Err(Error::BadChar { char: '/' }), + ), + ]; + + for (input, expected) in cases { + assert_eq!(&UriPathBuf::new(input).map(|t| t.0), expected) + } } } -- GitLab