use std::sync::OnceLock;
use regex::RegexSet;
static TRAVERSAL_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static TRAVERSAL_DESCRIPTIONS: &[&str] = &[
"Path traversal: ../",
"Path traversal: ..\\",
"URL-encoded traversal: %2e%2e",
"Double-encoded traversal: %252e",
"Null byte injection",
"Sensitive file: /etc/passwd",
"Sensitive file: /etc/shadow",
"Proc filesystem: /proc/self",
"Sensitive file: /etc/hosts",
"Windows system files",
"URL-encoded slash traversal",
];
fn patterns() -> &'static RegexSet {
TRAVERSAL_PATTERNS.get_or_init(|| {
RegexSet::new([
r"\.\./",
r"\.\.\x5c",
r"(?i)(%2e){2}[/%5c]",
r"(?i)(%252e){2}",
r"(?i)(%00|\\x00|\\0)",
r"(?i)/etc/passwd",
r"(?i)/etc/shadow",
r"(?i)/proc/self",
r"(?i)/etc/hosts",
r"(?i)(boot\.ini|win\.ini|system32|cmd\.exe)",
r"(?i)\.(%2f|%5c)\.",
])
.expect("traversal regex patterns must compile")
})
}
pub fn check_traversal(path: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(path).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
TRAVERSAL_DESCRIPTIONS
.get(idx)
.unwrap_or(&"path traversal")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_dot_dot_slash() {
assert!(check_traversal("../../etc/passwd").is_some());
assert!(check_traversal("/var/www/../../../etc/shadow").is_some());
}
#[test]
fn detects_dot_dot_backslash() {
assert!(check_traversal("..\\..\\windows\\system32").is_some());
}
#[test]
fn detects_url_encoded_traversal() {
assert!(check_traversal("%2e%2e%2f%2e%2e%2f").is_some());
assert!(check_traversal("%2e%2e/etc/passwd").is_some());
}
#[test]
fn detects_double_encoded() {
assert!(check_traversal("%252e%252e").is_some());
}
#[test]
fn detects_null_byte() {
assert!(check_traversal("/uploads/shell.php%00.jpg").is_some());
}
#[test]
fn detects_etc_passwd() {
assert!(check_traversal("/etc/passwd").is_some());
}
#[test]
fn detects_etc_shadow() {
assert!(check_traversal("/etc/shadow").is_some());
}
#[test]
fn detects_proc_self() {
assert!(check_traversal("/proc/self/environ").is_some());
}
#[test]
fn detects_windows_files() {
assert!(check_traversal("C:\\boot.ini").is_some());
assert!(check_traversal("cmd.exe /c dir").is_some());
}
#[test]
fn detects_mixed_encoding() {
assert!(check_traversal(".%2f.%2f.%2f").is_some());
}
#[test]
fn allows_normal_paths() {
assert!(check_traversal("/api/users/123").is_none());
assert!(check_traversal("/static/css/main.css").is_none());
}
#[test]
fn allows_dots_in_filenames() {
assert!(check_traversal("/uploads/image.v2.png").is_none());
}
#[test]
fn allows_normal_query_params() {
assert!(check_traversal("/search?q=hello+world").is_none());
}
}