1use anyhow::{bail, Result};
2
3pub const MAX_PATH_LENGTH: usize = 1024;
5
6pub const MAX_PATH_COMPONENTS: usize = 32;
8
9pub fn validate_request_path(path: &str) -> Result<String> {
11 if path.len() > MAX_PATH_LENGTH {
13 bail!("Path too long");
14 }
15
16 if !path.starts_with('/') {
18 bail!("Path must start with /");
19 }
20
21 let decoded = match urlencoding::decode(path) {
23 Ok(decoded) => decoded.into_owned(),
24 Err(_) => bail!("Invalid URL encoding"),
25 };
26
27 if decoded.contains('\0') {
29 bail!("Path contains null bytes");
30 }
31
32 let components: Vec<&str> = decoded.split('/').skip(1).collect(); if components.len() > MAX_PATH_COMPONENTS {
36 bail!("Too many path components");
37 }
38
39 let mut sanitized_components = Vec::new();
40
41 for component in components {
42 if component.is_empty() {
44 continue;
45 }
46
47 if component == ".." || component == "." {
49 bail!("Path traversal attempt detected");
50 }
51
52 if component.contains(['\\', '\0', '<', '>', '|', '?', '*']) {
54 bail!("Invalid characters in path component");
55 }
56
57 if component.starts_with('.') && component != ".well-known" {
59 bail!("Access to hidden files denied");
60 }
61
62 if component.len() > 255 {
64 bail!("Path component too long");
65 }
66
67 sanitized_components.push(component);
68 }
69
70 let safe_path = if sanitized_components.is_empty() {
72 "/".to_string()
73 } else {
74 format!("/{}", sanitized_components.join("/"))
75 };
76
77 Ok(safe_path)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn test_path_validation() {
86 assert!(validate_request_path("/").is_ok());
88 assert!(validate_request_path("/index.html").is_ok());
89 assert!(validate_request_path("/assets/style.css").is_ok());
90 assert!(validate_request_path("/.well-known/acme-challenge/token").is_ok());
91 assert!(validate_request_path("/.well-known/security.txt").is_ok());
92
93 assert!(validate_request_path("../etc/passwd").is_err());
95 assert!(validate_request_path("/.env").is_err());
96 assert!(validate_request_path("/.secret").is_err());
97 assert!(validate_request_path("/path/with/../../traversal").is_err());
98 assert!(validate_request_path("/path\0null").is_err());
99 }
100}