use wafrift_encoding::Strategy;
use wafrift_encoding::contextual::encode_in_context;
use wafrift_types::injection_context::InjectionContext;
#[test]
fn null_byte_json_string_escaped_not_rejected() {
let out = encode_in_context(
b"\x00",
Strategy::CaseAlternation,
InjectionContext::JsonString,
)
.unwrap();
assert_eq!(out, "\\u0000");
}
#[test]
fn null_byte_xml_attribute_rejected() {
let err = encode_in_context(
b"a\x00b",
Strategy::CaseAlternation,
InjectionContext::XmlAttribute,
)
.unwrap_err();
assert!(err.to_string().contains("incompatible"));
}
#[test]
fn null_byte_cookie_value_escaped() {
let out = encode_in_context(
b"a\x00b",
Strategy::CaseAlternation,
InjectionContext::CookieValue,
)
.unwrap();
assert!(out.contains("%00"));
}
#[test]
fn unicode_rtl_override_html_attribute() {
let out = encode_in_context(
"\u{202e}evil".as_bytes(),
Strategy::CaseAlternation,
InjectionContext::HtmlAttribute,
)
.unwrap();
assert!(out.contains('\u{202e}'));
}
#[test]
fn chinese_characters_json_string() {
let out = encode_in_context(
"中文测试".as_bytes(),
Strategy::CaseAlternation,
InjectionContext::JsonString,
)
.unwrap();
assert_eq!(out, "中文测试"); }
#[test]
fn emoji_json_string() {
let out = encode_in_context(
"Hello 👋".as_bytes(),
Strategy::CaseAlternation,
InjectionContext::JsonString,
)
.unwrap();
assert_eq!(out, "HeLlO 👋");
}
#[test]
fn surrogate_pairs_json_string() {
let out = encode_in_context(
"😀".as_bytes(),
Strategy::CaseAlternation,
InjectionContext::JsonString,
)
.unwrap();
assert_eq!(out, "😀");
}
#[test]
fn empty_payload_all_contexts() {
for ctx in all_contexts() {
let result = encode_in_context(b"", Strategy::CaseAlternation, ctx);
assert!(
result.is_ok(),
"empty payload failed for {ctx:?}: {result:?}"
);
}
}
#[test]
fn single_byte_payload_all_contexts() {
for ctx in all_contexts() {
let payload: &[u8] = if ctx == InjectionContext::JsonNumber {
b"1"
} else {
b"x"
};
let result = encode_in_context(payload, Strategy::CaseAlternation, ctx);
assert!(
result.is_ok(),
"single byte failed for {ctx:?}: {result:?}"
);
}
}
#[test]
fn max_size_minus_one_all_contexts() {
let sizes = vec![
(InjectionContext::JsonString, 4 * 1024 * 1024 - 1),
(InjectionContext::JsonNumber, 1023),
(InjectionContext::HeaderValue, 8 * 1024 - 1),
(InjectionContext::CookieValue, 4 * 1024 - 1),
(InjectionContext::MultipartFileName, 255),
];
for (ctx, size) in sizes {
let payload: Vec<u8> = if ctx == InjectionContext::JsonNumber {
vec![b'1'; size]
} else {
vec![b'x'; size]
};
assert!(
encode_in_context(&payload, Strategy::CaseAlternation, ctx).is_ok(),
"max_size-1 failed for {ctx:?}"
);
}
}
#[test]
fn max_size_plus_one_all_contexts() {
let sizes = vec![
(InjectionContext::JsonString, 4 * 1024 * 1024 + 1),
(InjectionContext::JsonNumber, 1025),
(InjectionContext::HeaderValue, 8 * 1024 + 1),
(InjectionContext::CookieValue, 4 * 1024 + 1),
(InjectionContext::MultipartFileName, 257),
];
for (ctx, size) in sizes {
let payload = vec![b'x'; size];
let err = encode_in_context(&payload, Strategy::CaseAlternation, ctx).unwrap_err();
assert!(
err.to_string().contains("too large"),
"wrong error for {ctx:?} at size {size}: {err}"
);
}
}
#[test]
fn header_value_crlf_injection_prevented() {
let err = encode_in_context(
b"value\r\nX-Injection: evil",
Strategy::CaseAlternation,
InjectionContext::HeaderValue,
)
.unwrap_err();
assert!(err.to_string().contains("incompatible"));
}
#[test]
fn multipart_field_crlf_injection_prevented() {
let err = encode_in_context(
b"field\r\n--boundary",
Strategy::CaseAlternation,
InjectionContext::MultipartField,
)
.unwrap_err();
assert!(err.to_string().contains("incompatible"));
}
#[test]
fn cookie_value_crlf_injection_prevented_indirectly() {
let out = encode_in_context(
b"a\r\nb",
Strategy::CaseAlternation,
InjectionContext::CookieValue,
)
.unwrap();
assert!(out.contains("%0D") || out.contains("%0A"));
}
#[test]
fn invalid_utf8_bytes_handled() {
let payload = vec![0x80];
let err = encode_in_context(
&payload,
Strategy::CaseAlternation,
InjectionContext::JsonString,
)
.unwrap_err();
assert!(err.to_string().contains("invalid"));
}
#[test]
fn overlong_utf8_sequence() {
let payload = vec![0xC0, 0x80];
let err = encode_in_context(
&payload,
Strategy::CaseAlternation,
InjectionContext::PlainBody,
)
.unwrap_err();
assert!(err.to_string().contains("invalid"));
}
#[test]
fn truncated_utf8_sequence() {
let payload = vec![0xE0];
let err = encode_in_context(
&payload,
Strategy::CaseAlternation,
InjectionContext::PlainBody,
)
.unwrap_err();
assert!(err.to_string().contains("invalid"));
}
#[test]
fn base64_in_json_string_produces_valid_json() {
let out = encode_in_context(
b"hello",
Strategy::Base64Encode,
InjectionContext::JsonString,
)
.unwrap();
assert!(!out.contains('"'));
assert!(!out.contains('\\'));
}
#[test]
fn html_entity_encode_in_xml_attribute_double_escapes() {
let out = encode_in_context(
b"<",
Strategy::HtmlEntityEncode,
InjectionContext::XmlAttribute,
)
.unwrap();
assert!(out.contains("&#x3C;"));
}
#[test]
fn chunked_split_in_header_value_rejected() {
let err = encode_in_context(
b"payload",
Strategy::ChunkedSplit,
InjectionContext::HeaderValue,
)
.unwrap_err();
assert!(err.to_string().contains("incompatible"));
}
#[test]
fn every_strategy_produces_valid_plain_body() {
let payload = b"<script>alert(1)</script>";
for &strategy in wafrift_encoding::all_strategies() {
let out = encode_in_context(payload, strategy, InjectionContext::PlainBody).unwrap();
assert!(
!out.is_empty(),
"strategy {strategy:?} produced empty output"
);
}
}
#[test]
fn every_strategy_produces_valid_url_query() {
let payload = b"hello world";
for &strategy in wafrift_encoding::all_strategies() {
let result = encode_in_context(payload, strategy, InjectionContext::UrlQuery);
if let Ok(out) = result {
assert!(
!out.contains(' '),
"strategy {strategy:?} produced raw space in URL query"
);
}
}
}
fn all_contexts() -> Vec<InjectionContext> {
use InjectionContext::*;
vec![
JsonString,
JsonNumber,
XmlAttribute,
XmlCdata,
XmlText,
HtmlAttribute,
HtmlText,
UrlQuery,
UrlPath,
UrlFragment,
HeaderValue,
CookieValue,
MultipartField,
MultipartFileName,
PlainBody,
]
}