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::UrlEncode)
92 | (Strategy::UrlEncode, Strategy::UrlEncodeLower)
93 | (Strategy::UrlEncodeLower, Strategy::UrlEncode)
94 | (Strategy::UrlEncodeLower, Strategy::UrlEncodeLower)
95 | (Strategy::DoubleUrlEncode, Strategy::UrlEncode)
96 | (Strategy::TripleUrlEncode, Strategy::UrlEncode)
97 | (Strategy::CaseAlternation, Strategy::RandomCase)
98 | (Strategy::RandomCase, Strategy::CaseAlternation)
99 )
100}
101
102#[must_use]
106pub fn aggressiveness(strategy: Strategy) -> f64 {
107 match strategy {
108 Strategy::CaseAlternation => 0.05,
109 Strategy::RandomCase => 0.08,
110 Strategy::UrlEncode => 0.1,
111 Strategy::UrlEncodeLower => 0.1,
112 Strategy::WhitespaceInsertion => 0.12,
113 Strategy::SqlCommentInsertion => 0.12,
114 Strategy::SpaceToPlus => 0.13,
115 Strategy::SpaceToRandomBlank => 0.14,
116 Strategy::SpaceToComment => 0.15,
117 Strategy::SpaceToDash => 0.15,
118 Strategy::SpaceToHash => 0.15,
119 Strategy::HtmlEntityEncode => 0.2,
120 Strategy::HtmlEntityDecimalEncode => 0.2,
121 Strategy::DoubleUrlEncode => 0.25,
122 Strategy::UnicodeEncode => 0.3,
123 Strategy::IisUnicodeEncode => 0.3,
124 Strategy::JsonEncode => 0.3,
125 Strategy::NullByte => 0.35,
126 Strategy::FullwidthEncode => 0.36,
127 Strategy::HomoglyphEncode => 0.37,
128 Strategy::PercentagePrefix => 0.4,
129 Strategy::ParameterPollution => 0.45,
130 Strategy::TripleUrlEncode => 0.5,
131 Strategy::MysqlVersionedComment => 0.55,
132 Strategy::Base64Encode => 0.6,
133 Strategy::Base64UrlEncode => 0.6,
134 Strategy::OverlongUtf8 => 0.7,
135 Strategy::OverlongUtf8More => 0.75,
136 Strategy::HexEncode => 0.8,
137 Strategy::Utf7Encode => 0.85,
138 Strategy::BetweenObfuscation => 0.88,
139 Strategy::UnmagicQuotes => 0.9,
140 Strategy::ChunkedSplit => 0.92,
141 Strategy::GzipEncode => 0.95,
142 Strategy::DeflateEncode => 0.95,
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::encoding::strategy::all_strategies;
150
151 #[test]
152 fn encode_layered_basic() {
153 let result =
154 encode_layered("A", &[Strategy::UrlEncode, Strategy::DoubleUrlEncode]).unwrap();
155 assert!(result.contains('%'));
156 }
157
158 #[test]
159 fn encode_layered_size_limit() {
160 let big = "!".repeat(5 * 1024 * 1024);
162 let result = encode_layered(
163 &big,
164 &[
165 Strategy::UrlEncode,
166 Strategy::UrlEncode,
167 Strategy::UrlEncode,
168 ],
169 );
170 assert!(matches!(
171 result,
172 Err(EncodeError::LayeredOutputTooLarge { .. })
173 ));
174 }
175
176 #[test]
177 fn layered_combinations_depth_2() {
178 let combos = layered_combinations(2);
179 assert!(!combos.is_empty());
180 assert!(combos.iter().all(|c| c.len() == 2));
182 }
183
184 #[test]
185 fn layered_combinations_no_consecutive_duplicates() {
186 let combos = layered_combinations(3);
187 for combo in combos {
188 for window in combo.windows(2) {
189 assert_ne!(window[0], window[1], "no consecutive duplicates: {combo:?}");
190 }
191 }
192 }
193
194 #[test]
195 fn aggressiveness_ordering() {
196 let strategies = all_strategies();
197 for i in 1..strategies.len() {
198 assert!(
199 aggressiveness(strategies[i - 1]) <= aggressiveness(strategies[i]),
200 "aggressiveness should be non-decreasing"
201 );
202 }
203 }
204
205 #[test]
206 fn encode_layered_empty_strategies() {
207 let result = encode_layered("hello", &[]).unwrap();
208 assert_eq!(result, "hello");
209 }
210
211 #[test]
212 fn encode_layered_single_strategy() {
213 let result = encode_layered("A<", &[Strategy::UrlEncode]).unwrap();
214 assert_eq!(result, "A%3C");
215 }
216
217 #[test]
218 fn layered_combinations_depth_1_returns_empty() {
219 let combos = layered_combinations(1);
220 assert!(combos.is_empty());
221 }
222
223 #[test]
224 fn aggressiveness_in_valid_range() {
225 for &s in all_strategies() {
226 let a = aggressiveness(s);
227 assert!(
228 (0.0..=1.0).contains(&a),
229 "aggressiveness for {s:?} out of range: {a}"
230 );
231 }
232 }
233}