use crate::traits::PayloadOracle;
use serde::Deserialize;
use std::sync::OnceLock;
pub struct PathOracle;
const PATH_TRAVERSAL_TOML: &str = include_str!("../rules/path_traversal/sequences.toml");
#[derive(Debug, Clone, Deserialize)]
struct TraversalSequence {
sequence: String,
#[allow(dead_code)]
description: String,
#[allow(dead_code)]
encoding: String,
}
#[derive(Debug, Clone, Deserialize)]
struct TargetFile {
path: String,
#[allow(dead_code)]
description: String,
#[allow(dead_code)]
os: String,
}
#[derive(Debug, Clone, Deserialize)]
struct PathTraversalRules {
#[serde(default)]
traversal_sequence: Vec<TraversalSequence>,
#[serde(default)]
target_file: Vec<TargetFile>,
}
fn get_rules() -> &'static PathTraversalRules {
static RULES: OnceLock<PathTraversalRules> = OnceLock::new();
RULES.get_or_init(|| {
toml::from_str(PATH_TRAVERSAL_TOML)
.expect("Failed to parse rules/path_traversal/sequences.toml - invalid TOML format")
})
}
fn traversal_sequences() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.traversal_sequence
.iter()
.map(|s| s.sequence.clone())
.collect()
})
}
fn target_files() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.target_file
.iter()
.map(|f| f.path.clone())
.collect()
})
}
fn has_traversal_structure(payload: &str) -> bool {
let lower = payload.to_ascii_lowercase();
let has_traversal = traversal_sequences()
.iter()
.any(|seq| lower.contains(&seq.to_ascii_lowercase()));
let has_target = target_files()
.iter()
.any(|target| lower.contains(&target.to_ascii_lowercase()));
has_traversal || (has_target && payload.contains(".."))
}
impl PayloadOracle for PathOracle {
fn is_semantically_valid(&self, _original: &str, transformed: &str) -> bool {
has_traversal_structure(transformed)
}
fn name(&self) -> &'static str {
"PathTraversal"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classic_traversal_valid() {
let oracle = PathOracle;
assert!(oracle.is_semantically_valid("../../../etc/passwd", "../../../etc/passwd",));
}
#[test]
fn encoded_traversal_valid() {
let oracle = PathOracle;
assert!(oracle.is_semantically_valid("../../../etc/passwd", "..%2f..%2f..%2fetc/passwd",));
}
#[test]
fn double_dot_semicolon_valid() {
let oracle = PathOracle;
assert!(oracle.is_semantically_valid("../../../etc/passwd", "..;/..;/..;/etc/passwd",));
}
#[test]
fn windows_traversal_valid() {
let oracle = PathOracle;
assert!(
oracle.is_semantically_valid("..\\..\\windows\\win.ini", "..\\..\\windows\\win.ini",)
);
}
#[test]
fn destroyed_dots_invalid() {
let oracle = PathOracle;
assert!(!oracle.is_semantically_valid("../../../etc/passwd", "XXXX/XXXX/XXXX/XXXX/XXXX",));
}
#[test]
fn plain_text_invalid() {
let oracle = PathOracle;
assert!(!oracle.is_semantically_valid("../../../etc/passwd", "hello world",));
}
#[test]
fn proc_self_environ_valid() {
let oracle = PathOracle;
assert!(
oracle
.is_semantically_valid("../../../proc/self/environ", "../../../proc/self/environ",)
);
}
#[test]
fn dot_env_valid() {
let oracle = PathOracle;
assert!(oracle.is_semantically_valid("../../.env", "../../.env",));
}
#[test]
fn null_byte_bypass_valid() {
let oracle = PathOracle;
assert!(
oracle.is_semantically_valid("../../../etc/passwd", "../../../etc/passwd\x00.jpg",)
);
}
}