1use std::fmt;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23#[non_exhaustive]
24pub enum HeaderTechnique {
25 CaseMixing,
27 TabSeparator,
29 WhitespacePadding,
31 LineFolding,
33 LfOnlyLineFolding,
35 DuplicateHeader,
37 UnderscoreSubstitution,
39 NullByteInjection,
41 TrailingSpace,
43 MultiLineFolding,
45 LfOnlyMultiLineFolding,
47 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#[must_use]
76pub fn case_mix(header_name: &str) -> String {
77 crate::encoding::keyword::alternating_case(header_name, false)
78}
79
80fn sanitize_header_value(value: &str) -> String {
89 value
90 .chars()
91 .filter(|c| *c != '\r' && *c != '\n' && *c != '\0')
92 .collect()
93}
94
95#[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#[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; 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#[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#[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#[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#[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#[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#[must_use]
209pub fn underscore_substitute(header_name: &str) -> String {
210 header_name.replace('-', "_")
211}
212
213#[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#[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#[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#[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;