use super::keyword::{
between_obfuscate, case_alternate, mysql_versioned_comment, percentage_prefix,
random_case_alternate, space_to_comment, space_to_dash, space_to_hash, space_to_plus,
space_to_random_blank, sql_comment_insert, unmagic_quotes, whitespace_insert,
};
use super::structural::{
base64_encode, base64_url_encode, chunked_split, deflate_encode, gzip_encode, hex_encode,
null_byte_inject, overlong_utf8, overlong_utf8_more, parameter_pollute, utf7_encode,
};
use super::unicode::{
fullwidth_encode, homoglyph_encode, html_entity_decimal_encode, html_entity_encode,
iis_unicode_encode, json_string_encode, unicode_encode,
};
use super::url::{double_url_encode, triple_url_encode, url_encode, url_encode_lower};
use crate::error::EncodeError;
pub const MAX_PAYLOAD_SIZE: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum Strategy {
UrlEncode,
UrlEncodeLower,
DoubleUrlEncode,
TripleUrlEncode,
UnicodeEncode,
IisUnicodeEncode,
JsonEncode,
HtmlEntityEncode,
HtmlEntityDecimalEncode,
CaseAlternation,
RandomCase,
WhitespaceInsertion,
SqlCommentInsertion,
MysqlVersionedComment,
NullByte,
OverlongUtf8,
OverlongUtf8More,
ChunkedSplit,
ParameterPollution,
Base64Encode,
Base64UrlEncode,
HexEncode,
Utf7Encode,
GzipEncode,
DeflateEncode,
SpaceToComment,
SpaceToDash,
SpaceToHash,
SpaceToPlus,
SpaceToRandomBlank,
PercentagePrefix,
BetweenObfuscation,
UnmagicQuotes,
FullwidthEncode,
HomoglyphEncode,
}
impl Strategy {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::UrlEncode => "UrlEncode",
Self::UrlEncodeLower => "UrlEncodeLower",
Self::DoubleUrlEncode => "DoubleUrlEncode",
Self::TripleUrlEncode => "TripleUrlEncode",
Self::UnicodeEncode => "UnicodeEncode",
Self::IisUnicodeEncode => "IisUnicodeEncode",
Self::JsonEncode => "JsonEncode",
Self::HtmlEntityEncode => "HtmlEntityEncode",
Self::HtmlEntityDecimalEncode => "HtmlEntityDecimalEncode",
Self::CaseAlternation => "CaseAlternation",
Self::RandomCase => "RandomCase",
Self::WhitespaceInsertion => "WhitespaceInsertion",
Self::SqlCommentInsertion => "SqlCommentInsertion",
Self::MysqlVersionedComment => "MysqlVersionedComment",
Self::NullByte => "NullByte",
Self::OverlongUtf8 => "OverlongUtf8",
Self::OverlongUtf8More => "OverlongUtf8More",
Self::ChunkedSplit => "ChunkedSplit",
Self::ParameterPollution => "ParameterPollution",
Self::Base64Encode => "Base64Encode",
Self::Base64UrlEncode => "Base64UrlEncode",
Self::HexEncode => "HexEncode",
Self::Utf7Encode => "Utf7Encode",
Self::GzipEncode => "GzipEncode",
Self::DeflateEncode => "DeflateEncode",
Self::SpaceToComment => "SpaceToComment",
Self::SpaceToDash => "SpaceToDash",
Self::SpaceToHash => "SpaceToHash",
Self::SpaceToPlus => "SpaceToPlus",
Self::SpaceToRandomBlank => "SpaceToRandomBlank",
Self::PercentagePrefix => "PercentagePrefix",
Self::BetweenObfuscation => "BetweenObfuscation",
Self::UnmagicQuotes => "UnmagicQuotes",
Self::FullwidthEncode => "FullwidthEncode",
Self::HomoglyphEncode => "HomoglyphEncode",
}
}
#[must_use]
pub const fn contexts(&self) -> &'static [&'static str] {
match self {
Self::UrlEncode
| Self::UrlEncodeLower
| Self::DoubleUrlEncode
| Self::TripleUrlEncode
| Self::ParameterPollution => &[],
Self::UnicodeEncode => &["json", "javascript"],
Self::IisUnicodeEncode => &["iis", "asp"],
Self::JsonEncode => &["json"],
Self::HtmlEntityEncode | Self::HtmlEntityDecimalEncode => &["html"],
Self::CaseAlternation | Self::RandomCase | Self::WhitespaceInsertion => &[],
Self::SqlCommentInsertion
| Self::MysqlVersionedComment
| Self::SpaceToComment
| Self::SpaceToDash
| Self::SpaceToRandomBlank
| Self::BetweenObfuscation => &["sql"],
Self::SpaceToHash => &["sql", "mysql"],
Self::SpaceToPlus => &["url-encoded"],
Self::NullByte => &["php", "cgi"],
Self::OverlongUtf8 | Self::OverlongUtf8More => &["iis-6"],
Self::ChunkedSplit => &["http-request-body"],
Self::Base64Encode | Self::Base64UrlEncode | Self::HexEncode => &[],
Self::Utf7Encode => &["iis", "legacy-dotnet"],
Self::GzipEncode | Self::DeflateEncode => &["http-request-body"],
Self::PercentagePrefix => &[],
Self::UnmagicQuotes => &["php", "gbk", "big5", "shift-jis"],
Self::FullwidthEncode => &["nfkc", "java", "dotnet", "python3", "postgresql"],
Self::HomoglyphEncode => &[],
}
}
}
fn check_size(payload: &[u8]) -> Result<(), EncodeError> {
if payload.len() > MAX_PAYLOAD_SIZE {
Err(EncodeError::PayloadTooLarge {
max: MAX_PAYLOAD_SIZE,
actual: payload.len(),
})
} else {
Ok(())
}
}
pub fn encode(payload: impl AsRef<[u8]>, strategy: Strategy) -> Result<String, EncodeError> {
let payload = payload.as_ref();
check_size(payload)?;
match strategy {
Strategy::UrlEncode => Ok(url_encode(payload)),
Strategy::UrlEncodeLower => Ok(url_encode_lower(payload)),
Strategy::DoubleUrlEncode => Ok(double_url_encode(payload)),
Strategy::TripleUrlEncode => Ok(triple_url_encode(payload)),
Strategy::UnicodeEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(unicode_encode(text))
}
Strategy::IisUnicodeEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(iis_unicode_encode(text))
}
Strategy::JsonEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(json_string_encode(text))
}
Strategy::HtmlEntityEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(html_entity_encode(text))
}
Strategy::HtmlEntityDecimalEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(html_entity_decimal_encode(text))
}
Strategy::CaseAlternation => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(case_alternate(text))
}
Strategy::RandomCase => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(random_case_alternate(text))
}
Strategy::WhitespaceInsertion => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(whitespace_insert(text))
}
Strategy::SqlCommentInsertion => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(sql_comment_insert(text))
}
Strategy::MysqlVersionedComment => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(mysql_versioned_comment(text, 50_000))
}
Strategy::NullByte => Ok(null_byte_inject(payload)?),
Strategy::OverlongUtf8 => Ok(overlong_utf8(payload)?),
Strategy::OverlongUtf8More => Ok(overlong_utf8_more(payload)?),
Strategy::ChunkedSplit => {
let body = chunked_split(payload, 1024)?.body;
String::from_utf8(body).map_err(|_| EncodeError::InvalidUtf8)
}
Strategy::ParameterPollution => Ok(parameter_pollute(payload)?),
Strategy::Base64Encode => Ok(base64_encode(payload)),
Strategy::Base64UrlEncode => Ok(base64_url_encode(payload)),
Strategy::HexEncode => Ok(hex_encode(payload)),
Strategy::Utf7Encode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(utf7_encode(text))
}
Strategy::GzipEncode => Ok(gzip_encode(payload)?),
Strategy::DeflateEncode => Ok(deflate_encode(payload)?),
Strategy::SpaceToComment => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(space_to_comment(text))
}
Strategy::SpaceToDash => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(space_to_dash(text))
}
Strategy::SpaceToHash => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(space_to_hash(text))
}
Strategy::SpaceToPlus => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(space_to_plus(text))
}
Strategy::SpaceToRandomBlank => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(space_to_random_blank(text))
}
Strategy::PercentagePrefix => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(percentage_prefix(text))
}
Strategy::BetweenObfuscation => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(between_obfuscate(text))
}
Strategy::UnmagicQuotes => Ok(unmagic_quotes(payload)?),
Strategy::FullwidthEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(fullwidth_encode(text))
}
Strategy::HomoglyphEncode => {
let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
Ok(homoglyph_encode(text))
}
}
}
static ALL_STRATEGIES: std::sync::LazyLock<Vec<Strategy>> = std::sync::LazyLock::new(|| {
let mut strategies = vec![
Strategy::CaseAlternation,
Strategy::RandomCase,
Strategy::WhitespaceInsertion,
Strategy::SqlCommentInsertion,
Strategy::SpaceToPlus,
Strategy::SpaceToRandomBlank,
Strategy::SpaceToComment,
Strategy::SpaceToDash,
Strategy::SpaceToHash,
Strategy::UrlEncode,
Strategy::UrlEncodeLower,
Strategy::DoubleUrlEncode,
Strategy::UnicodeEncode,
Strategy::IisUnicodeEncode,
Strategy::JsonEncode,
Strategy::HtmlEntityEncode,
Strategy::HtmlEntityDecimalEncode,
Strategy::NullByte,
Strategy::PercentagePrefix,
Strategy::TripleUrlEncode,
Strategy::ChunkedSplit,
Strategy::ParameterPollution,
Strategy::MysqlVersionedComment,
Strategy::Base64Encode,
Strategy::Base64UrlEncode,
Strategy::OverlongUtf8,
Strategy::OverlongUtf8More,
Strategy::HexEncode,
Strategy::Utf7Encode,
Strategy::BetweenObfuscation,
Strategy::UnmagicQuotes,
Strategy::FullwidthEncode,
Strategy::HomoglyphEncode,
Strategy::GzipEncode,
Strategy::DeflateEncode,
];
strategies.sort_by(|a, b| {
super::layered::aggressiveness(*a)
.partial_cmp(&super::layered::aggressiveness(*b))
.unwrap_or(std::cmp::Ordering::Equal)
});
strategies
});
#[must_use]
pub fn all_strategies() -> &'static [Strategy] {
&ALL_STRATEGIES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_url_encode_basic() {
assert_eq!(encode("A<", Strategy::UrlEncode).unwrap(), "A%3C");
}
#[test]
fn encode_url_encode_lower() {
assert_eq!(encode("A<", Strategy::UrlEncodeLower).unwrap(), "A%3c");
}
#[test]
fn encode_double_url_encode() {
assert_eq!(
encode("A<", Strategy::DoubleUrlEncode).unwrap(),
"%2541%253C"
);
}
#[test]
fn encode_case_alternation() {
let result = encode("SELECT", Strategy::CaseAlternation).unwrap();
assert!(result.contains("SeL") || result.contains("sEl"));
}
#[test]
fn encode_null_byte() {
let result = encode("file.php", Strategy::NullByte).unwrap();
assert!(result.contains('\x00') || result.contains("%00"));
}
#[test]
fn encode_base64() {
assert_eq!(encode("hello", Strategy::Base64Encode).unwrap(), "aGVsbG8=");
}
#[test]
fn encode_hex() {
assert_eq!(encode("ABC", Strategy::HexEncode).unwrap(), "414243");
}
#[test]
fn encode_json() {
assert_eq!(encode("A<", Strategy::JsonEncode).unwrap(), "\"A<\"");
}
#[test]
fn encode_html_entity() {
assert_eq!(
encode("A<", Strategy::HtmlEntityEncode).unwrap(),
"A<"
);
}
#[test]
fn encode_invalid_utf8_fails() {
let invalid = vec![0x80, 0x81, 0x82];
let result = encode(&invalid, Strategy::CaseAlternation);
assert!(matches!(result, Err(EncodeError::InvalidUtf8)));
}
#[test]
fn encode_payload_too_large_fails() {
let huge = vec![b'X'; MAX_PAYLOAD_SIZE + 1];
let result = encode(&huge, Strategy::UrlEncode);
assert!(matches!(result, Err(EncodeError::PayloadTooLarge { .. })));
}
#[test]
fn all_strategies_non_empty() {
let strategies = all_strategies();
assert!(!strategies.is_empty());
assert!(strategies.contains(&Strategy::UrlEncode));
}
#[test]
fn strategy_as_str_roundtrip() {
for s in all_strategies() {
assert!(!s.as_str().is_empty());
}
}
#[test]
fn strategy_contexts_returns_slice() {
assert!(Strategy::UrlEncode.contexts().is_empty());
assert_eq!(Strategy::JsonEncode.contexts(), &["json"]);
assert_eq!(Strategy::SpaceToComment.contexts(), &["sql"]);
}
#[test]
fn encode_empty_payload() {
assert_eq!(encode("", Strategy::UrlEncode).unwrap(), "");
}
#[test]
fn encode_unicode() {
let result = encode("A<", Strategy::UnicodeEncode).unwrap();
assert!(result.contains("\\u"));
}
#[test]
fn encode_chunked_split() {
let result = encode("hello", Strategy::ChunkedSplit).unwrap();
assert!(result.contains("\r\n"));
assert!(result.ends_with("0\r\n\r\n"));
}
#[test]
fn encode_parameter_pollution() {
let result = encode("key=value", Strategy::ParameterPollution).unwrap();
assert!(result.contains("key="));
}
#[test]
fn encode_gzip_produces_base64() {
let result = encode("hello", Strategy::GzipEncode).unwrap();
assert!(!result.is_empty());
}
#[test]
fn encode_iis_unicode() {
let result = encode("A<", Strategy::IisUnicodeEncode).unwrap();
assert!(result.contains("%u"));
}
}