wafrift_encoding/encoding/
structural.rs1use base64::{Engine as _, engine::general_purpose};
4use std::io::Write as _;
5
6use crate::error::EncodeError;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ChunkedBody {
14 pub body: Vec<u8>,
16 pub required_headers: Vec<(String, String)>,
18}
19
20pub fn null_byte_inject(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
25 let payload = payload.as_ref();
26 let payload_str = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
27 if payload.contains(&b'.') {
28 Ok(format!("{payload_str}%00.jpg"))
29 } else {
30 Ok(format!("{payload_str}%00"))
31 }
32}
33
34pub fn overlong_utf8(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
39 let text = std::str::from_utf8(payload.as_ref()).map_err(|_| EncodeError::InvalidUtf8)?;
40 Ok(text
41 .chars()
42 .map(|ch| {
43 if ch.is_ascii_alphanumeric() {
44 ch.to_string()
45 } else if ch.is_ascii() {
46 let byte = ch as u8;
47 format!("%{:02X}%{:02X}", 0xC0 | (byte >> 6), 0x80 | (byte & 0x3F))
48 } else {
49 ch.to_string()
50 }
51 })
52 .collect())
53}
54
55pub fn overlong_utf8_more(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
59 let text = std::str::from_utf8(payload.as_ref()).map_err(|_| EncodeError::InvalidUtf8)?;
60 Ok(text
61 .chars()
62 .map(|ch| {
63 if ch.is_ascii_alphanumeric() {
64 ch.to_string()
65 } else if ch.is_ascii() {
66 let byte = ch as u8;
67 format!("%{:02X}%{:02X}%{:02X}", 0xE0, 0x80, 0x80 | byte)
68 } else {
69 ch.to_string()
70 }
71 })
72 .collect())
73}
74
75pub fn chunked_split(
80 payload: impl AsRef<[u8]>,
81 chunk_size: usize,
82) -> Result<ChunkedBody, EncodeError> {
83 let payload = payload.as_ref();
84 if payload.is_empty() {
85 return Ok(ChunkedBody {
86 body: Vec::new(),
87 required_headers: vec![("Transfer-Encoding".to_string(), "chunked".to_string())],
88 });
89 }
90 let chunk_size = chunk_size.max(1);
91 let mut result: Vec<u8> = Vec::with_capacity(payload.len() + 64);
92
93 for chunk in payload.chunks(chunk_size) {
94 let _ = write!(&mut result, "{:x}\r\n", chunk.len());
95 result.extend_from_slice(chunk);
96 result.extend_from_slice(b"\r\n");
97 }
98 result.extend_from_slice(b"0\r\n\r\n");
99
100 Ok(ChunkedBody {
101 body: result,
102 required_headers: vec![("Transfer-Encoding".to_string(), "chunked".to_string())],
103 })
104}
105
106pub fn parameter_pollute(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
111 let payload = payload.as_ref();
112 let payload_str = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
113 if let Some(eq_pos) = payload.iter().position(|byte| *byte == b'=') {
114 let key = std::str::from_utf8(&payload[..eq_pos]).map_err(|_| EncodeError::InvalidUtf8)?;
115 Ok(format!("{key}=safe&{payload_str}"))
116 } else {
117 let decoy: String = (0..8)
118 .map(|_| rand::random::<u8>() % 26 + b'a')
119 .map(|b| b as char)
120 .collect();
121 Ok(format!("{decoy}=1&{payload_str}"))
122 }
123}
124
125pub fn base64_encode(payload: impl AsRef<[u8]>) -> String {
127 general_purpose::STANDARD.encode(payload)
128}
129
130pub fn base64_url_encode(payload: impl AsRef<[u8]>) -> String {
132 general_purpose::URL_SAFE_NO_PAD.encode(payload)
133}
134
135pub fn hex_encode(payload: impl AsRef<[u8]>) -> String {
137 hex::encode(payload)
138}
139
140fn char_to_utf16be(c: char) -> Vec<u8> {
142 let mut buf = [0u16; 2];
143 let enc = c.encode_utf16(&mut buf);
144 let mut out = Vec::with_capacity(enc.len() * 2);
145 for u in enc {
146 out.push((*u >> 8) as u8);
147 out.push((*u & 0xFF) as u8);
148 }
149 out
150}
151
152fn modified_base64(bytes: &[u8]) -> String {
154 let mut b64 = general_purpose::STANDARD.encode(bytes);
155 b64.retain(|c| c != '=');
156 b64
157}
158
159fn is_utf7_direct(ch: char) -> bool {
161 matches!(
162 ch,
163 'A'..='Z'
164 | 'a'..='z'
165 | '0'..='9'
166 | '\''
167 | '('
168 | ')'
169 | ','
170 | '-'
171 | '.'
172 | '/'
173 | ':'
174 | '?'
175 )
176}
177
178pub fn utf7_encode(payload: &str) -> String {
183 let mut out = String::new();
184 let mut shift_buf: Vec<u8> = Vec::new();
185
186 fn flush_shift(out: &mut String, buf: &mut Vec<u8>) {
187 if !buf.is_empty() {
188 out.push('+');
189 out.push_str(&modified_base64(buf));
190 out.push('-');
191 buf.clear();
192 }
193 }
194
195 for ch in payload.chars() {
196 if ch == '+' {
197 flush_shift(&mut out, &mut shift_buf);
198 out.push_str("+-");
199 } else if is_utf7_direct(ch) {
200 flush_shift(&mut out, &mut shift_buf);
201 out.push(ch);
202 } else {
203 shift_buf.extend_from_slice(&char_to_utf16be(ch));
204 }
205 }
206 flush_shift(&mut out, &mut shift_buf);
207 out
208}
209
210pub fn gzip_encode(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
214 let payload = payload.as_ref();
215 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
216 encoder
217 .write_all(payload)
218 .map_err(|e| EncodeError::InvalidConfig(format!("gzip failed: {e}")))?;
219 let bytes = encoder
220 .finish()
221 .map_err(|e| EncodeError::InvalidConfig(format!("gzip failed: {e}")))?;
222 Ok(general_purpose::STANDARD.encode(bytes))
223}
224
225pub fn deflate_encode(payload: impl AsRef<[u8]>) -> Result<String, EncodeError> {
229 let payload = payload.as_ref();
230 let mut encoder =
231 flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
232 encoder
233 .write_all(payload)
234 .map_err(|e| EncodeError::InvalidConfig(format!("deflate failed: {e}")))?;
235 let bytes = encoder
236 .finish()
237 .map_err(|e| EncodeError::InvalidConfig(format!("deflate failed: {e}")))?;
238 Ok(general_purpose::STANDARD.encode(bytes))
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn null_byte_with_extension() {
247 assert_eq!(null_byte_inject("file.php").unwrap(), "file.php%00.jpg");
248 }
249
250 #[test]
251 fn null_byte_without_extension() {
252 assert_eq!(null_byte_inject("payload").unwrap(), "payload%00");
253 }
254
255 #[test]
256 fn overlong_utf8_slash() {
257 let result = overlong_utf8("/").unwrap();
258 assert_eq!(result, "%C0%AF");
259 }
260
261 #[test]
262 fn overlong_utf8_more_slash() {
263 let result = overlong_utf8_more("/").unwrap();
264 assert_eq!(result, "%E0%80%AF");
265 }
266
267 #[test]
268 fn chunked_split_produces_valid_chunks() {
269 let result = chunked_split("SELECT * FROM users", 3).unwrap();
270 let body = String::from_utf8(result.body.clone()).unwrap();
271 assert!(body.contains("\r\n"));
272 assert!(body.ends_with("0\r\n\r\n"));
273 assert_eq!(
274 result.required_headers,
275 vec![("Transfer-Encoding".to_string(), "chunked".to_string())]
276 );
277 }
278
279 #[test]
280 fn chunked_split_byte_lengths_correct() {
281 let payload = b"abc\x80\x81defgh";
282 let result = chunked_split(payload, 3).unwrap();
283 let mut i = 0;
285 let mut chunk_count = 0;
286 let expected_chunk_sizes = [3_usize, 3, 3, 1];
287 while i < result.body.len() {
288 let size_end = result.body[i..]
290 .windows(2)
291 .position(|w| w == b"\r\n")
292 .unwrap_or(result.body.len() - i)
293 + i;
294 let size_str = std::str::from_utf8(&result.body[i..size_end]).unwrap();
295 if size_str == "0" {
296 break;
298 }
299 let size = usize::from_str_radix(size_str, 16).unwrap();
300 assert_eq!(size, expected_chunk_sizes[chunk_count]);
301 let data_start = size_end + 2;
303 let data_end = data_start + size;
304 assert_eq!(
305 &result.body[data_start..data_end],
306 &payload[chunk_count * 3..chunk_count * 3 + size]
307 );
308 i = data_end + 2;
310 chunk_count += 1;
311 }
312 assert_eq!(chunk_count, 4);
313 }
314
315 #[test]
316 fn chunked_split_empty() {
317 let result = chunked_split("", 3).unwrap();
318 assert!(result.body.is_empty());
319 }
320
321 #[test]
322 fn parameter_pollution_with_key_value() {
323 let result = parameter_pollute("user=' OR 1=1--").unwrap();
324 assert!(result.starts_with("user=safe&"));
325 assert!(result.contains("user=' OR 1=1--"));
326 }
327
328 #[test]
329 fn parameter_pollution_without_equals() {
330 let result = parameter_pollute("payload").unwrap();
331 assert!(result.ends_with("&payload"));
332 assert!(!result.contains("_wafrift_decoy"));
333 }
334
335 #[test]
336 fn base64_standard() {
337 assert_eq!(base64_encode("hello"), "aGVsbG8=");
338 }
339
340 #[test]
341 fn base64_url_safe() {
342 assert_eq!(base64_url_encode("hello+++"), "aGVsbG8rKys");
343 }
344
345 #[test]
346 fn hex_encode_basic() {
347 assert_eq!(hex_encode("ABC"), "414243");
348 }
349
350 #[test]
351 fn utf7_rfc2152_basic() {
352 assert_eq!(utf7_encode("Hello"), "Hello");
354 assert_eq!(utf7_encode("A+B"), "A+-B");
356 assert!(utf7_encode("日本語").starts_with('+'));
358 }
359
360 #[test]
361 fn utf7_rfc2152_decodeable() {
362 let encoded = utf7_encode("日本語");
364 assert!(encoded.contains('+'));
365 assert!(encoded.contains('-'));
366 }
367
368 #[test]
369 fn gzip_roundtrip() {
370 let original = b"SELECT * FROM users";
371 let encoded = gzip_encode(original).unwrap();
372 assert!(!encoded.is_empty());
373 let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
375 let mut decoder = flate2::read::GzDecoder::new(&decoded[..]);
376 let mut decompressed = Vec::new();
377 std::io::Read::read_to_end(&mut decoder, &mut decompressed).unwrap();
378 assert_eq!(decompressed, original);
379 }
380
381 #[test]
382 fn deflate_roundtrip() {
383 let original = b"SELECT * FROM users";
384 let encoded = deflate_encode(original).unwrap();
385 assert!(!encoded.is_empty());
386 let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
387 let mut decoder = flate2::read::DeflateDecoder::new(&decoded[..]);
388 let mut decompressed = Vec::new();
389 std::io::Read::read_to_end(&mut decoder, &mut decompressed).unwrap();
390 assert_eq!(decompressed, original);
391 }
392}