ferrox_guards/
squeezer.rs1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SqueezerAssessment {
14 pub is_evasion_detected: bool,
15 pub original_length: usize,
16 pub squeezed_length: usize,
17 pub zero_width_chars_removed: usize,
18 pub squeezed_payload: String,
19 pub rationale: String,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct FeatureSqueezer;
25
26impl FeatureSqueezer {
27 pub fn new() -> Self {
28 Self
29 }
30
31 pub fn squeeze_payload(&self, input: &str) -> SqueezerAssessment {
33 let original_len = input.len();
34 let mut zero_width_count = 0;
35 let mut cleaned = String::with_capacity(original_len);
36
37 for ch in input.chars() {
39 match ch {
40 '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{200E}' | '\u{200F}' | '\u{FEFF}' | '\u{00AD}' => {
41 zero_width_count += 1;
42 }
43 c if c.is_control() && c != '\n' && c != '\r' && c != '\t' => {
44 zero_width_count += 1;
45 }
46 c => {
47 cleaned.push(c);
48 }
49 }
50 }
51
52 let unencoded = Self::decode_url_percent(&cleaned);
54
55 let squeezed = Self::compress_whitespace(&unencoded);
57
58 let is_evasion = zero_width_count > 0 || (original_len > 0 && (squeezed.len() as f64 / original_len as f64) < 0.6);
59
60 let rationale = if zero_width_count > 0 {
61 format!("Removed {} zero-width/control characters used in evasion attempt", zero_width_count)
62 } else if is_evasion {
63 "Extreme payload compression ratio detected".to_string()
64 } else {
65 "Payload clean and canonicalized".to_string()
66 };
67
68 SqueezerAssessment {
69 is_evasion_detected: is_evasion,
70 original_length: original_len,
71 squeezed_length: squeezed.len(),
72 zero_width_chars_removed: zero_width_count,
73 squeezed_payload: squeezed,
74 rationale,
75 }
76 }
77
78 fn compress_whitespace(s: &str) -> String {
79 let mut result = String::with_capacity(s.len());
80 let mut in_space = false;
81 for c in s.chars() {
82 if c.is_whitespace() {
83 if !in_space {
84 result.push(' ');
85 in_space = true;
86 }
87 } else {
88 result.push(c);
89 in_space = false;
90 }
91 }
92 result.trim().to_string()
93 }
94
95 fn decode_url_percent(s: &str) -> String {
96 let mut out = String::with_capacity(s.len());
97 let bytes = s.as_bytes();
98 let mut i = 0;
99 while i < bytes.len() {
100 if bytes[i] == b'%' && i + 2 < bytes.len() {
101 if let (Some(h1), Some(h2)) = (Self::hex_val(bytes[i + 1]), Self::hex_val(bytes[i + 2])) {
102 let decoded_byte = (h1 << 4) | h2;
103 out.push(decoded_byte as char);
104 i += 3;
105 continue;
106 }
107 }
108 out.push(bytes[i] as char);
109 i += 1;
110 }
111 out
112 }
113
114 fn hex_val(b: u8) -> Option<u8> {
115 match b {
116 b'0'..=b'9' => Some(b - b'0'),
117 b'a'..=b'f' => Some(b - b'a' + 10),
118 b'A'..=b'F' => Some(b - b'A' + 10),
119 _ => None,
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_squeezer_zero_width_evasion() {
130 let squeezer = FeatureSqueezer::new();
131 let malicious_input = "S\u{200B}E\u{200B}L\u{200B}E\u{200B}C\u{200B}T * FROM users";
133 let res = squeezer.squeeze_payload(malicious_input);
134
135 assert!(res.is_evasion_detected);
136 assert_eq!(res.zero_width_chars_removed, 5);
137 assert_eq!(res.squeezed_payload, "SELECT * FROM users");
138 }
139
140 #[test]
141 fn test_squeezer_percent_encoding_canonicalization() {
142 let squeezer = FeatureSqueezer::new();
143 let input = "UNION%20SELECT%201,%202";
144 let res = squeezer.squeeze_payload(input);
145
146 assert_eq!(res.squeezed_payload, "UNION SELECT 1, 2");
147 }
148}