diff --git a/src/config.rs b/src/config.rs index 298dec4..0b19946 100644 --- a/src/config.rs +++ b/src/config.rs @@ -170,15 +170,23 @@ fn normalize_and_parse_upstream_url(mut url: String) -> Url { } impl HttpConfig { - pub fn create_upstream_url(&self, req: &HttpRequest) -> String { - let base = Url::parse(self.upstream_base_url.as_ref()).unwrap(); - let mut url = base.join(&req.match_info()["name"]).unwrap(); + pub fn create_upstream_url(&self, req: &HttpRequest) -> Option { + // Warning: join process '../' + // "https://a.com/jail/".join('../escape') => "https://a.com/escape" + let mut url = self + .upstream_base_url + .join(&req.match_info()["name"]) + .unwrap(); + + if self.is_traversal_attack(&url) { + return None; + } if !req.query_string().is_empty() { url.set_query(Some(req.query_string())); } - url.to_string() + Some(url.to_string()) } pub fn local_encryption_path_for(&self, req: &HttpRequest) -> PathBuf { @@ -187,6 +195,27 @@ impl HttpConfig { filepath.push(name); filepath } + + fn is_traversal_attack(&self, url: &Url) -> bool { + // https://upstream.com => [Some("")] + // https://upstream.com/jail/cell/ => [Some("jail"), Some("cell"), Some("")] + let mut base_segments: Vec<&str> = + self.upstream_base_url.path_segments().unwrap().collect(); + + // remove the last segment corresponding to "/" + base_segments.pop(); + + let mut url_segments = url.path_segments().unwrap(); + + // ensure that all segment of the upstream_base_url + // are present in the final url + let safe = base_segments.iter().all(|base_segment| { + let url_segment = url_segments.next().unwrap(); + base_segment == &url_segment + }); + + !safe + } } fn read_file_content(path_string: &str) -> String { @@ -213,34 +242,65 @@ mod tests { #[test] fn test_create_upstream_url() { - let req = TestRequest::default() - .uri("https://proxy.com/bucket/file.zip?p1=ok1&p2=ok2") - .param("name", "bucket/file.zip") // hack to force parsing + let base = "https://upstream.com/"; + let jailed_base = "https://upstream.com/jail/cell/"; + + let config = default_config(base); + let jailed_config = default_config(jailed_base); + + let file = TestRequest::default() + .uri("https://proxy.com/file") + .param("name", "file") // hack to force parsing .to_http_request(); assert_eq!( - default_config("https://upstream.com").create_upstream_url(&req), - "https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2" + config.create_upstream_url(&file), + Some("https://upstream.com/file".to_string()) ); assert_eq!( - default_config("https://upstream.com/").create_upstream_url(&req), - "https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2" + jailed_config.create_upstream_url(&file), + Some("https://upstream.com/jail/cell/file".to_string()) + ); + + let sub_dir_file = TestRequest::default() + .uri("https://proxy.com/sub/dir/file") + .param("name", "sub/dir/file") // hack to force parsing + .to_http_request(); + + assert_eq!( + config.create_upstream_url(&sub_dir_file), + Some("https://upstream.com/sub/dir/file".to_string()) ); assert_eq!( - default_config("https://upstream.com/sub_folder/").create_upstream_url(&req), - "https://upstream.com/sub_folder/bucket/file.zip?p1=ok1&p2=ok2" + jailed_config.create_upstream_url(&sub_dir_file), + Some("https://upstream.com/jail/cell/sub/dir/file".to_string()) ); - let req = TestRequest::default() - .uri("https://proxy.com/bucket/file.zip") + let path_traversal_file = TestRequest::default() + .uri("https://proxy.com/../escape") + .param("name", "../escape") // hack to force parsing + .to_http_request(); + + assert_eq!( + config.create_upstream_url(&path_traversal_file), + Some("https://upstream.com/escape".to_string()) + ); + + assert_eq!( + jailed_config.create_upstream_url(&path_traversal_file), + None + ); + + let file_with_query_string = TestRequest::default() + .uri("https://proxy.com/bucket/file.zip?p1=ok1&p2=ok2") .param("name", "bucket/file.zip") // hack to force parsing .to_http_request(); assert_eq!( - default_config("https://upstream.com").create_upstream_url(&req), - "https://upstream.com/bucket/file.zip" + config.create_upstream_url(&file_with_query_string), + Some("https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2".to_string()) ); } diff --git a/src/http/handlers/fetch.rs b/src/http/handlers/fetch.rs index 50d756e..5c1596a 100644 --- a/src/http/handlers/fetch.rs +++ b/src/http/handlers/fetch.rs @@ -11,8 +11,12 @@ pub async fn fetch( ) -> Result { let get_url = config.create_upstream_url(&req); + if get_url.is_none() { + return not_found(); + } + let mut fetch_req = client - .request_from(get_url.as_str(), req.head()) + .request_from(get_url.unwrap(), req.head()) .force_close(); let raw_range = req diff --git a/src/http/handlers/forward.rs b/src/http/handlers/forward.rs index 0751ba8..e4e797a 100644 --- a/src/http/handlers/forward.rs +++ b/src/http/handlers/forward.rs @@ -30,8 +30,12 @@ pub async fn forward( ) -> Result { let put_url = config.create_upstream_url(&req); + if put_url.is_none() { + return not_found(); + } + let mut forwarded_req = client - .request_from(put_url.as_str(), req.head()) + .request_from(put_url.unwrap(), req.head()) .force_close() .timeout(UPLOAD_TIMEOUT); diff --git a/src/http/handlers/mod.rs b/src/http/handlers/mod.rs index 278d33e..d12f052 100644 --- a/src/http/handlers/mod.rs +++ b/src/http/handlers/mod.rs @@ -36,3 +36,11 @@ pub static FETCH_REQUEST_HEADERS_TO_REMOVE: [header::HeaderName; 2] = [ header::CONNECTION, header::RANGE, ]; + +pub fn not_found() -> Result { + let response = HttpResponse::NotFound() + .insert_header((header::CONTENT_TYPE, "application/json")) + .body("{}"); + + Ok(response) +} diff --git a/src/http/handlers/simple_proxy.rs b/src/http/handlers/simple_proxy.rs index 49b352d..1cd7ee0 100644 --- a/src/http/handlers/simple_proxy.rs +++ b/src/http/handlers/simple_proxy.rs @@ -8,7 +8,11 @@ pub async fn simple_proxy( ) -> Result { let url = config.create_upstream_url(&req); - let mut proxied_req = client.request_from(url.as_str(), req.head()).force_close(); + if url.is_none() { + return not_found(); + } + + let mut proxied_req = client.request_from(url.unwrap(), req.head()).force_close(); for header in &FETCH_REQUEST_HEADERS_TO_REMOVE { proxied_req.headers_mut().remove(header);