Skip to main content

wafrift_encoding/
header.rs

1//! HTTP header obfuscation for WAF bypass.
2//!
3//! WAFs inspect HTTP headers to detect malicious requests. This module
4//! applies transformations that are valid per HTTP RFCs but confuse
5//! WAF header parsers, causing them to misparse or skip inspection.
6//!
7//! # Techniques
8//!
9//! - **Case mixing** — `cOnTeNt-TyPe` instead of `Content-Type`
10//! - **Whitespace tricks** — tabs, spaces around colons and values
11//! - **Header folding** — obsolete but still parsed by many servers (RFC 7230 §3.2.4)
12//! - **Duplicate headers** — first vs. last wins disagreement
13//! - **Underscore substitution** — `Content_Type` accepted by some servers
14//! - **Null byte injection** — `Content-Type\x00` truncates header name
15//! - **`SPaced` header name** — `Content-Type ` trailing space before colon
16//! - **Header value wrapping** — Value spread across multiple continuation lines
17//! - **Comma-joined header values** — Multiple values in one header via comma
18
19use std::fmt;
20
21/// A header transformation technique.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23#[non_exhaustive]
24pub enum HeaderTechnique {
25    /// Random case mixing of header name.
26    CaseMixing,
27    /// Tab character instead of space after colon.
28    TabSeparator,
29    /// Extra whitespace around header value.
30    WhitespacePadding,
31    /// Obsolete header folding with continuation line (CRLF + whitespace).
32    LineFolding,
33    /// LF-only continuation line.
34    LfOnlyLineFolding,
35    /// Duplicate header with benign value first.
36    DuplicateHeader,
37    /// Underscore instead of hyphen in header name.
38    UnderscoreSubstitution,
39    /// Null byte injected into header name.
40    NullByteInjection,
41    /// Trailing space before colon in header name.
42    TrailingSpace,
43    /// Header value wrapped across multiple continuation lines.
44    MultiLineFolding,
45    /// LF-only multi-line folding.
46    LfOnlyMultiLineFolding,
47    /// Multiple values comma-joined in a single header.
48    CommaJoin,
49}
50
51impl fmt::Display for HeaderTechnique {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::CaseMixing => f.write_str("case-mixing"),
55            Self::TabSeparator => f.write_str("tab-separator"),
56            Self::WhitespacePadding => f.write_str("whitespace-padding"),
57            Self::LineFolding => f.write_str("line-folding"),
58            Self::LfOnlyLineFolding => f.write_str("lf-only-line-folding"),
59            Self::DuplicateHeader => f.write_str("duplicate-header"),
60            Self::UnderscoreSubstitution => f.write_str("underscore-substitution"),
61            Self::NullByteInjection => f.write_str("null-byte-injection"),
62            Self::TrailingSpace => f.write_str("trailing-space"),
63            Self::MultiLineFolding => f.write_str("multi-line-folding"),
64            Self::LfOnlyMultiLineFolding => f.write_str("lf-only-multi-line-folding"),
65            Self::CommaJoin => f.write_str("comma-join"),
66        }
67    }
68}
69
70/// Apply case mixing to a header name.
71///
72/// Produces `cOnTeNt-TyPe` style output. HTTP header names are defined
73/// as case-insensitive (RFC 7230 §3.2), so servers accept any casing,
74/// but some WAFs only match canonical `Content-Type`.
75#[must_use]
76pub fn case_mix(header_name: &str) -> String {
77    crate::encoding::keyword::alternating_case(header_name, false)
78}
79
80/// Apply tab separator: `Header:\tvalue` instead of `Header: value`.
81#[must_use]
82pub fn tab_separator(header_name: &str, value: &str) -> String {
83    format!("{header_name}:\t{value}")
84}
85
86/// Apply whitespace padding around the value.
87#[must_use]
88pub fn whitespace_pad(header_name: &str, value: &str) -> String {
89    let pad_count = rand::random::<usize>() % 4 + 2; // 2–5 spaces
90    let left = " ".repeat(pad_count);
91    let right = " ".repeat(pad_count);
92    format!("{header_name}:{left}{value}{right}")
93}
94
95fn char_boundary_near(s: &str, byte_idx: usize) -> usize {
96    if byte_idx >= s.len() {
97        return s.len();
98    }
99    let mut i = byte_idx;
100    while i > 0 && !s.is_char_boundary(i) {
101        i -= 1;
102    }
103    i
104}
105
106/// Apply obsolete line folding (RFC 7230 §3.2.4).
107///
108/// The header value is split across two lines with a continuation marker
109/// (CRLF followed by a space or tab). This is obsolete but many servers
110/// still accept it, while WAFs often do not reassemble folded headers.
111#[must_use]
112pub fn line_fold(header_name: &str, value: &str) -> String {
113    line_fold_with_ending(header_name, value, "\r\n")
114}
115
116/// Apply LF-only line folding.
117#[must_use]
118pub fn lf_only_line_fold(header_name: &str, value: &str) -> String {
119    line_fold_with_ending(header_name, value, "\n")
120}
121
122fn line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
123    if value.len() < 4 {
124        return format!("{header_name}: {value}");
125    }
126    let mid = char_boundary_near(value, value.len() / 2);
127    format!(
128        "{}: {}{ending}\t{}",
129        header_name,
130        &value[..mid],
131        &value[mid..]
132    )
133}
134
135/// Apply multi-line folding — value spread across 3+ continuation lines.
136///
137/// More aggressive than single fold — splits value into thirds.
138/// Many WAFs only handle one continuation line.
139#[must_use]
140pub fn multi_line_fold(header_name: &str, value: &str) -> String {
141    multi_line_fold_with_ending(header_name, value, "\r\n")
142}
143
144/// Apply LF-only multi-line folding.
145#[must_use]
146pub fn lf_only_multi_line_fold(header_name: &str, value: &str) -> String {
147    multi_line_fold_with_ending(header_name, value, "\n")
148}
149
150fn multi_line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
151    if value.len() < 6 {
152        return format!("{header_name}: {value}");
153    }
154    let t1 = char_boundary_near(value, value.len() / 3);
155    let t2 = char_boundary_near(value, value.len() * 2 / 3);
156    format!(
157        "{}: {}{ending} {}{ending}\t{}",
158        header_name,
159        &value[..t1],
160        &value[t1..t2],
161        &value[t2..]
162    )
163}
164
165/// Generate a duplicate header pair: returns `(benign_line, real_line)`.
166///
167/// Some WAFs only inspect the first occurrence of a header, while many
168/// servers use the last. By placing a benign value first and the real
169/// value second, the WAF sees the benign header, the server sees the
170/// real one.
171#[must_use]
172pub fn duplicate_header(
173    header_name: &str,
174    real_value: &str,
175    benign_value: &str,
176) -> (String, String) {
177    (
178        format!("{header_name}: {benign_value}"),
179        format!("{header_name}: {real_value}"),
180    )
181}
182
183/// Replace hyphens with underscores in the header name.
184///
185/// Some web servers (notably PHP with `$_SERVER`, and CGI) normalise
186/// `Content_Type` → `Content-Type`. WAFs typically do not.
187#[must_use]
188pub fn underscore_substitute(header_name: &str) -> String {
189    header_name.replace('-', "_")
190}
191
192/// Inject a null byte into the header name at the midpoint.
193///
194/// Some C-based WAF implementations (modSecurity, native nginx modules)
195/// use null-terminated string operations internally. A null byte in the
196/// header name causes the WAF to see a truncated name (e.g., `Content`
197/// instead of `Content-Type\x00`), while the upstream server may parse
198/// the full name.
199#[must_use]
200pub fn null_byte_inject(header_name: &str) -> String {
201    if header_name.len() < 2 {
202        return header_name.to_string();
203    }
204    let mid = char_boundary_near(header_name, header_name.len() / 2);
205    format!("{}\x00{}", &header_name[..mid], &header_name[mid..])
206}
207
208/// Add a trailing space before the colon separator.
209///
210/// `Content-Type : value` — some parsers strip the space, making this
211/// equivalent. WAFs that expect `Name:` or `Name: ` without extra space
212/// in the header name field may fail to match.
213#[must_use]
214pub fn trailing_space(header_name: &str, value: &str) -> String {
215    format!("{header_name} : {value}")
216}
217
218/// Comma-join multiple values into a single header.
219///
220/// Per RFC 7230 §3.2.6, a recipient may combine multiple header fields
221/// with the same name into one `field-value` separated by commas.
222/// `Header: benign, malicious` is semantically equivalent to two
223/// separate `Header: benign` and `Header: malicious` lines. WAFs that
224/// split on the first comma may only inspect `benign`.
225#[must_use]
226pub fn comma_join(header_name: &str, real_value: &str, benign_value: &str) -> String {
227    format!("{header_name}: {benign_value}, {real_value}")
228}
229
230/// Apply all header obfuscation techniques to a header name/value pair.
231///
232/// Returns a vector of `(technique, obfuscated_header_line)` pairs.
233/// For `DuplicateHeader`, the two lines are joined with CRLF.
234#[must_use]
235pub fn all_obfuscations(header_name: &str, value: &str) -> Vec<(HeaderTechnique, String)> {
236    let benign = "safe_value";
237    vec![
238        (
239            HeaderTechnique::CaseMixing,
240            format!("{}: {}", case_mix(header_name), value),
241        ),
242        (
243            HeaderTechnique::TabSeparator,
244            tab_separator(header_name, value),
245        ),
246        (
247            HeaderTechnique::WhitespacePadding,
248            whitespace_pad(header_name, value),
249        ),
250        (HeaderTechnique::LineFolding, line_fold(header_name, value)),
251        (
252            HeaderTechnique::LfOnlyLineFolding,
253            lf_only_line_fold(header_name, value),
254        ),
255        (HeaderTechnique::DuplicateHeader, {
256            let (a, b) = duplicate_header(header_name, value, benign);
257            format!("{a}\r\n{b}")
258        }),
259        (
260            HeaderTechnique::UnderscoreSubstitution,
261            format!("{}: {}", underscore_substitute(header_name), value),
262        ),
263        (
264            HeaderTechnique::NullByteInjection,
265            format!("{}: {}", null_byte_inject(header_name), value),
266        ),
267        (
268            HeaderTechnique::TrailingSpace,
269            trailing_space(header_name, value),
270        ),
271        (
272            HeaderTechnique::MultiLineFolding,
273            multi_line_fold(header_name, value),
274        ),
275        (
276            HeaderTechnique::LfOnlyMultiLineFolding,
277            lf_only_multi_line_fold(header_name, value),
278        ),
279        (
280            HeaderTechnique::CommaJoin,
281            comma_join(header_name, value, benign),
282        ),
283    ]
284}
285
286#[cfg(test)]
287#[path = "header_tests.rs"]
288mod tests;