use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mutation {
Truncate { keep_chars: usize },
PadWithFiller { len: usize, fill_char: u8 },
CharSwap { swap_count: usize, replacement: u8 },
StripPrefix,
StripDots,
RepeatSegment { segment_len: usize, repeat: usize },
InjectHtml,
InjectWhitespace,
SwapBase64ToBase64url,
HexOnlyConvert,
ZeroBody,
SingleCharBody { c: u8 },
JsonShape,
WrongPrefix { prefix: &'static str },
InjectZeroWidth,
InjectRtlOverride,
PrefixOnly,
}
impl Mutation {
pub fn apply(&self, token: &str) -> String {
match *self {
Mutation::Truncate { keep_chars } => token.chars().take(keep_chars).collect(),
Mutation::PadWithFiller { len, fill_char } => {
let mut out = token.to_string();
for _ in 0..len {
out.push(fill_char as char);
}
out
}
Mutation::CharSwap {
swap_count,
replacement,
} => {
let mut chars: Vec<char> = token.chars().collect();
let r = replacement as char;
for i in 0..swap_count.min(chars.len()) {
let pos = (i * 7) % chars.len();
chars[pos] = r;
}
chars.iter().collect()
}
Mutation::StripPrefix => strip_prefix(token).to_string(),
Mutation::StripDots => token.replace('.', ""),
Mutation::RepeatSegment {
segment_len,
repeat,
} => {
if token.is_empty() || segment_len == 0 {
return token.to_string();
}
let body = strip_prefix(token);
let seg: String = body.chars().take(segment_len).collect();
let mut out = vendor_prefix(token).to_string();
for _ in 0..repeat {
out.push_str(&seg);
}
out
}
Mutation::InjectHtml => {
let mid = utf8_safe_mid(token);
let (left, right) = token.split_at(mid);
format!("{left}<script>alert(1)</script>{right}")
}
Mutation::InjectWhitespace => {
let mid = utf8_safe_mid(token);
let (left, right) = token.split_at(mid);
format!("{left}\n\t\n{right}")
}
Mutation::SwapBase64ToBase64url => token.replace('-', "+").replace('_', "/"),
Mutation::HexOnlyConvert => token
.chars()
.filter(|c| c.is_ascii_hexdigit() || *c == '.')
.collect(),
Mutation::ZeroBody => {
let prefix = vendor_prefix(token);
let body_len = token.len().saturating_sub(prefix.len());
let mut out = prefix.to_string();
for _ in 0..body_len {
out.push('0');
}
out
}
Mutation::SingleCharBody { c } => {
let prefix = vendor_prefix(token);
let body_len = token.len().saturating_sub(prefix.len());
let mut out = prefix.to_string();
let cs = c as char;
for _ in 0..body_len {
out.push(cs);
}
out
}
Mutation::JsonShape => {
format!(
"{{\"verified\":true,\"score\":1.0,\"action\":\"submit\",\"token_len\":{}}}",
token.len()
)
}
Mutation::WrongPrefix { prefix } => {
let body = strip_prefix(token);
format!("{prefix}{body}")
}
Mutation::InjectZeroWidth => {
let mid = utf8_safe_mid(token);
let (left, right) = token.split_at(mid);
format!("{left}\u{200B}{right}")
}
Mutation::InjectRtlOverride => {
let mid = utf8_safe_mid(token);
let (left, right) = token.split_at(mid);
format!("{left}\u{202E}{right}")
}
Mutation::PrefixOnly => vendor_prefix(token).to_string(),
}
}
pub fn name(&self) -> &'static str {
match self {
Mutation::Truncate { .. } => "truncate",
Mutation::PadWithFiller { .. } => "pad_with_filler",
Mutation::CharSwap { .. } => "char_swap",
Mutation::StripPrefix => "strip_prefix",
Mutation::StripDots => "strip_dots",
Mutation::RepeatSegment { .. } => "repeat_segment",
Mutation::InjectHtml => "inject_html",
Mutation::InjectWhitespace => "inject_whitespace",
Mutation::SwapBase64ToBase64url => "swap_base64_to_base64url",
Mutation::HexOnlyConvert => "hex_only",
Mutation::ZeroBody => "zero_body",
Mutation::SingleCharBody { .. } => "single_char_body",
Mutation::JsonShape => "json_shape",
Mutation::WrongPrefix { .. } => "wrong_prefix",
Mutation::InjectZeroWidth => "inject_zero_width",
Mutation::InjectRtlOverride => "inject_rtl_override",
Mutation::PrefixOnly => "prefix_only",
}
}
}
fn vendor_prefix(token: &str) -> &str {
for p in &[
"03AGdBq25_",
"03AGdBq27_",
"P0_",
"P1_",
"0.",
"1.",
"9.",
"datadome_cookie=",
"aws-waf-token=",
"_abck=",
"_px3=",
] {
if token.starts_with(p) {
return &token[..p.len()];
}
}
""
}
fn strip_prefix(token: &str) -> &str {
let prefix = vendor_prefix(token);
&token[prefix.len()..]
}
fn utf8_safe_mid(token: &str) -> usize {
let target = token.len() / 2;
for i in target..=token.len() {
if token.is_char_boundary(i) {
return i;
}
}
token.len()
}
pub fn standard_mutations() -> Vec<Mutation> {
vec![
Mutation::Truncate { keep_chars: 5 },
Mutation::Truncate { keep_chars: 20 },
Mutation::PadWithFiller {
len: 200,
fill_char: b'a',
},
Mutation::PadWithFiller {
len: 400,
fill_char: b'0',
},
Mutation::CharSwap {
swap_count: 50,
replacement: b' ',
},
Mutation::CharSwap {
swap_count: 50,
replacement: b'<',
},
Mutation::StripPrefix,
Mutation::StripDots,
Mutation::RepeatSegment {
segment_len: 3,
repeat: 100,
},
Mutation::InjectHtml,
Mutation::InjectWhitespace,
Mutation::SwapBase64ToBase64url,
Mutation::HexOnlyConvert,
Mutation::ZeroBody,
Mutation::SingleCharBody { c: b'a' },
Mutation::SingleCharBody { c: b'X' },
Mutation::JsonShape,
Mutation::WrongPrefix { prefix: "9." },
Mutation::WrongPrefix { prefix: "ZZZ_" },
Mutation::InjectZeroWidth,
Mutation::InjectRtlOverride,
Mutation::PrefixOnly,
]
}
pub fn generate(seed: &str, mutations: &[Mutation]) -> Vec<(&'static str, String)> {
mutations
.iter()
.map(|m| (m.name(), m.apply(seed)))
.collect()
}
#[cfg(test)]
#[path = "adversarial_decoy/tests.rs"]
mod tests;