wafrift_encoding/encoding/
layered.rs1use super::strategy::{Strategy, all_strategies, encode};
4use crate::error::EncodeError;
5
6pub const MAX_LAYERED_OUTPUT_SIZE: usize = 8 * 1024 * 1024;
8
9pub fn encode_layered(
16 payload: impl AsRef<[u8]>,
17 strategies: &[Strategy],
18) -> Result<String, EncodeError> {
19 let payload = payload.as_ref();
20 let mut result = encode(
21 payload,
22 strategies.first().copied().unwrap_or(Strategy::UrlEncode),
23 )?;
24
25 for strategy in strategies.iter().skip(1) {
26 if result.len() > MAX_LAYERED_OUTPUT_SIZE {
27 return Err(EncodeError::LayeredOutputTooLarge {
28 max: MAX_LAYERED_OUTPUT_SIZE,
29 actual: result.len(),
30 });
31 }
32 result = encode(&result, *strategy)?;
33 }
34
35 if result.len() > MAX_LAYERED_OUTPUT_SIZE {
36 return Err(EncodeError::LayeredOutputTooLarge {
37 max: MAX_LAYERED_OUTPUT_SIZE,
38 actual: result.len(),
39 });
40 }
41
42 Ok(result)
43}
44
45pub fn layered_combinations(depth: usize) -> Vec<Vec<Strategy>> {
50 let base = all_strategies();
51 let mut results: Vec<Vec<Strategy>> = Vec::new();
52
53 fn backtrack(
54 base: &[Strategy],
55 current: &mut Vec<Strategy>,
56 results: &mut Vec<Vec<Strategy>>,
57 depth: usize,
58 ) {
59 if current.len() >= 2 && current.len() <= depth {
60 results.push(current.clone());
61 }
62 if current.len() >= depth {
63 return;
64 }
65 for s in base {
66 if current.last() == Some(s) {
68 continue;
69 }
70 if let Some(last) = current.last()
72 && redundant_pair(*last, *s)
73 {
74 continue;
75 }
76 current.push(*s);
77 backtrack(base, current, results, depth);
78 current.pop();
79 }
80 }
81
82 let mut current = Vec::new();
83 backtrack(base, &mut current, &mut results, depth);
84 results
85}
86
87fn redundant_pair(a: Strategy, b: Strategy) -> bool {
88 matches!(
90 (a, b),
91 (Strategy::UrlEncode | Strategy::UrlEncodeLower | Strategy::DoubleUrlEncode |
92Strategy::TripleUrlEncode, Strategy::UrlEncode) |
93(Strategy::UrlEncode | Strategy::UrlEncodeLower, Strategy::UrlEncodeLower) |
94(Strategy::CaseAlternation, Strategy::RandomCase) |
95(Strategy::RandomCase, Strategy::CaseAlternation)
96 )
97}
98
99#[must_use]
103pub fn aggressiveness(strategy: Strategy) -> f64 {
104 match strategy {
105 Strategy::CaseAlternation => 0.05,
106 Strategy::RandomCase => 0.08,
107 Strategy::UrlEncode => 0.1,
108 Strategy::UrlEncodeLower => 0.1,
109 Strategy::WhitespaceInsertion => 0.12,
110 Strategy::SqlCommentInsertion => 0.12,
111 Strategy::SpaceToPlus => 0.13,
112 Strategy::SpaceToRandomBlank => 0.14,
113 Strategy::SpaceToComment => 0.15,
114 Strategy::SpaceToDash => 0.15,
115 Strategy::SpaceToHash => 0.15,
116 Strategy::HtmlEntityEncode => 0.2,
117 Strategy::HtmlEntityDecimalEncode => 0.2,
118 Strategy::DoubleUrlEncode => 0.25,
119 Strategy::UnicodeEncode => 0.3,
120 Strategy::IisUnicodeEncode => 0.3,
121 Strategy::JsonEncode => 0.3,
122 Strategy::NullByte => 0.35,
123 Strategy::FullwidthEncode => 0.36,
124 Strategy::HomoglyphEncode => 0.37,
125 Strategy::PercentagePrefix => 0.4,
126 Strategy::ParameterPollution => 0.45,
127 Strategy::TripleUrlEncode => 0.5,
128 Strategy::MysqlVersionedComment => 0.55,
129 Strategy::Base64Encode => 0.6,
130 Strategy::Base64UrlEncode => 0.6,
131 Strategy::OverlongUtf8 => 0.7,
132 Strategy::OverlongUtf8More => 0.75,
133 Strategy::HexEncode => 0.8,
134 Strategy::Utf7Encode => 0.85,
135 Strategy::BetweenObfuscation => 0.88,
136 Strategy::UnmagicQuotes => 0.9,
137 Strategy::ChunkedSplit => 0.92,
138 Strategy::GzipEncode => 0.95,
139 Strategy::DeflateEncode => 0.95,
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::encoding::strategy::all_strategies;
147
148 #[test]
149 fn encode_layered_basic() {
150 let result =
151 encode_layered("A", &[Strategy::UrlEncode, Strategy::DoubleUrlEncode]).unwrap();
152 assert!(result.contains('%'));
153 }
154
155 #[test]
156 fn encode_layered_size_limit() {
157 let big = "!".repeat(5 * 1024 * 1024);
159 let result = encode_layered(
160 &big,
161 &[
162 Strategy::UrlEncode,
163 Strategy::UrlEncode,
164 Strategy::UrlEncode,
165 ],
166 );
167 assert!(matches!(
168 result,
169 Err(EncodeError::LayeredOutputTooLarge { .. })
170 ));
171 }
172
173 #[test]
174 fn layered_combinations_depth_2() {
175 let combos = layered_combinations(2);
176 assert!(!combos.is_empty());
177 assert!(combos.iter().all(|c| c.len() == 2));
179 }
180
181 #[test]
182 fn layered_combinations_no_consecutive_duplicates() {
183 let combos = layered_combinations(3);
184 for combo in combos {
185 for window in combo.windows(2) {
186 assert_ne!(window[0], window[1], "no consecutive duplicates: {combo:?}");
187 }
188 }
189 }
190
191 #[test]
192 fn aggressiveness_ordering() {
193 let strategies = all_strategies();
194 for i in 1..strategies.len() {
195 assert!(
196 aggressiveness(strategies[i - 1]) <= aggressiveness(strategies[i]),
197 "aggressiveness should be non-decreasing"
198 );
199 }
200 }
201
202 #[test]
203 fn encode_layered_empty_strategies() {
204 let result = encode_layered("hello", &[]).unwrap();
205 assert_eq!(result, "hello");
206 }
207
208 #[test]
209 fn encode_layered_single_strategy() {
210 let result = encode_layered("A<", &[Strategy::UrlEncode]).unwrap();
211 assert_eq!(result, "A%3C");
212 }
213
214 #[test]
215 fn layered_combinations_depth_1_returns_empty() {
216 let combos = layered_combinations(1);
217 assert!(combos.is_empty());
218 }
219
220 #[test]
221 fn aggressiveness_in_valid_range() {
222 for &s in all_strategies() {
223 let a = aggressiveness(s);
224 assert!(
225 (0.0..=1.0).contains(&a),
226 "aggressiveness for {s:?} out of range: {a}"
227 );
228 }
229 }
230}