use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum HeaderTechnique {
CaseMixing,
TabSeparator,
WhitespacePadding,
LineFolding,
LfOnlyLineFolding,
DuplicateHeader,
UnderscoreSubstitution,
NullByteInjection,
TrailingSpace,
MultiLineFolding,
LfOnlyMultiLineFolding,
CommaJoin,
}
impl fmt::Display for HeaderTechnique {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CaseMixing => f.write_str("case-mixing"),
Self::TabSeparator => f.write_str("tab-separator"),
Self::WhitespacePadding => f.write_str("whitespace-padding"),
Self::LineFolding => f.write_str("line-folding"),
Self::LfOnlyLineFolding => f.write_str("lf-only-line-folding"),
Self::DuplicateHeader => f.write_str("duplicate-header"),
Self::UnderscoreSubstitution => f.write_str("underscore-substitution"),
Self::NullByteInjection => f.write_str("null-byte-injection"),
Self::TrailingSpace => f.write_str("trailing-space"),
Self::MultiLineFolding => f.write_str("multi-line-folding"),
Self::LfOnlyMultiLineFolding => f.write_str("lf-only-multi-line-folding"),
Self::CommaJoin => f.write_str("comma-join"),
}
}
}
#[must_use]
pub fn case_mix(header_name: &str) -> String {
crate::encoding::keyword::alternating_case(header_name, false)
}
fn sanitize_header_value(value: &str) -> String {
value
.chars()
.filter(|c| *c != '\r' && *c != '\n' && *c != '\0')
.collect()
}
#[must_use]
pub fn tab_separator(header_name: &str, value: &str) -> String {
let value = sanitize_header_value(value);
format!("{header_name}:\t{value}")
}
#[must_use]
pub fn whitespace_pad(header_name: &str, value: &str) -> String {
let value = sanitize_header_value(value);
let pad_count = rand::random::<usize>() % 4 + 2; let left = " ".repeat(pad_count);
let right = " ".repeat(pad_count);
format!("{header_name}:{left}{value}{right}")
}
fn char_boundary_near(s: &str, byte_idx: usize) -> usize {
if byte_idx >= s.len() {
return s.len();
}
let mut i = byte_idx;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
#[must_use]
pub fn line_fold(header_name: &str, value: &str) -> String {
line_fold_with_ending(header_name, value, "\r\n")
}
#[must_use]
pub fn lf_only_line_fold(header_name: &str, value: &str) -> String {
line_fold_with_ending(header_name, value, "\n")
}
fn line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
let value = sanitize_header_value(value);
if value.len() < 4 {
return format!("{header_name}: {value}");
}
let mid = char_boundary_near(&value, value.len() / 2);
format!(
"{}: {}{ending}\t{}",
header_name,
&value[..mid],
&value[mid..]
)
}
#[must_use]
pub fn multi_line_fold(header_name: &str, value: &str) -> String {
multi_line_fold_with_ending(header_name, value, "\r\n")
}
#[must_use]
pub fn lf_only_multi_line_fold(header_name: &str, value: &str) -> String {
multi_line_fold_with_ending(header_name, value, "\n")
}
fn multi_line_fold_with_ending(header_name: &str, value: &str, ending: &str) -> String {
let value = sanitize_header_value(value);
if value.len() < 6 {
return format!("{header_name}: {value}");
}
let t1 = char_boundary_near(&value, value.len() / 3);
let t2 = char_boundary_near(&value, value.len() * 2 / 3);
format!(
"{}: {}{ending} {}{ending}\t{}",
header_name,
&value[..t1],
&value[t1..t2],
&value[t2..]
)
}
#[must_use]
pub fn duplicate_header(
header_name: &str,
real_value: &str,
benign_value: &str,
) -> (String, String) {
let real = sanitize_header_value(real_value);
let benign = sanitize_header_value(benign_value);
(
format!("{header_name}: {benign}"),
format!("{header_name}: {real}"),
)
}
#[must_use]
pub fn underscore_substitute(header_name: &str) -> String {
header_name.replace('-', "_")
}
#[must_use]
pub fn null_byte_inject(header_name: &str) -> String {
if header_name.len() < 2 {
return header_name.to_string();
}
let mid = char_boundary_near(header_name, header_name.len() / 2);
format!("{}\x00{}", &header_name[..mid], &header_name[mid..])
}
#[must_use]
pub fn trailing_space(header_name: &str, value: &str) -> String {
let value = sanitize_header_value(value);
format!("{header_name} : {value}")
}
#[must_use]
pub fn comma_join(header_name: &str, real_value: &str, benign_value: &str) -> String {
let real = sanitize_header_value(real_value);
let benign = sanitize_header_value(benign_value);
format!("{header_name}: {benign}, {real}")
}
#[must_use]
pub fn all_obfuscations(header_name: &str, value: &str) -> Vec<(HeaderTechnique, String)> {
let benign = "safe_value";
vec![
(
HeaderTechnique::CaseMixing,
format!("{}: {}", case_mix(header_name), value),
),
(
HeaderTechnique::TabSeparator,
tab_separator(header_name, value),
),
(
HeaderTechnique::WhitespacePadding,
whitespace_pad(header_name, value),
),
(HeaderTechnique::LineFolding, line_fold(header_name, value)),
(
HeaderTechnique::LfOnlyLineFolding,
lf_only_line_fold(header_name, value),
),
(HeaderTechnique::DuplicateHeader, {
let (a, b) = duplicate_header(header_name, value, benign);
format!("{a}\r\n{b}")
}),
(
HeaderTechnique::UnderscoreSubstitution,
format!("{}: {}", underscore_substitute(header_name), value),
),
(
HeaderTechnique::NullByteInjection,
format!("{}: {}", null_byte_inject(header_name), value),
),
(
HeaderTechnique::TrailingSpace,
trailing_space(header_name, value),
),
(
HeaderTechnique::MultiLineFolding,
multi_line_fold(header_name, value),
),
(
HeaderTechnique::LfOnlyMultiLineFolding,
lf_only_multi_line_fold(header_name, value),
),
(
HeaderTechnique::CommaJoin,
comma_join(header_name, value, benign),
),
]
}
#[cfg(test)]
#[path = "header_tests.rs"]
mod tests;