#![allow(clippy::unwrap_used)] #![allow(clippy::default_trait_access)] use fraiseql_core::db::{
WhereSqlGenerator,
where_clause::{WhereClause, WhereOperator},
};
use serde_json::json;
const INJECTION_PAYLOADS: &[&str] = &[
"'; DROP TABLE users; --",
"' OR '1'='1",
"admin'--",
"' UNION SELECT * FROM passwords --",
"1; DELETE FROM users WHERE '1'='1",
"') OR ('1'='1",
"\" OR \"\"=\"\"",
"' OR 1=1 --",
"admin' OR 'a'='a",
"1' UNION SELECT NULL, NULL, NULL --",
"' OR 'a'='a",
"admin' #",
"' /**/OR/**/1=1 --",
"1' AND '1'='1",
"' AND 1=1 --",
];
fn assert_injection_safe(sql: &str, payload: &str, context: &str) {
if payload.contains('\'') {
let escaped_payload = payload.replace('\'', "''");
assert!(
sql.contains(&escaped_payload),
"{context}: single quotes in payload must be doubled.\n payload: {payload}\n sql: {sql}"
);
}
let dangerous_patterns = [
"DROP TABLE",
"DELETE FROM",
"INSERT INTO",
"UNION SELECT",
"WAITFOR DELAY",
];
for pattern in dangerous_patterns {
if payload.contains(pattern) {
assert!(
sql.contains('\''),
"{context}: SQL must contain string literals for value with {pattern}"
);
}
}
}
#[test]
fn test_eq_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["email".to_string()],
operator: WhereOperator::Eq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, payload, "Eq operator");
}
}
#[test]
fn test_neq_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["email".to_string()],
operator: WhereOperator::Neq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("!="), "Neq must use != operator: {sql}");
assert_injection_safe(&sql, payload, "Neq operator");
}
}
#[test]
fn test_contains_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["comment".to_string()],
operator: WhereOperator::Contains,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("LIKE"), "Contains must use LIKE: {sql}");
assert_injection_safe(&sql, payload, "Contains operator");
}
}
#[test]
fn test_icontains_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: WhereOperator::Icontains,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("ILIKE"), "Icontains must use ILIKE: {sql}");
assert_injection_safe(&sql, payload, "Icontains operator");
}
}
#[test]
fn test_startswith_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["username".to_string()],
operator: WhereOperator::Startswith,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("LIKE"), "Startswith must use LIKE: {sql}");
assert_injection_safe(&sql, payload, "Startswith operator");
}
}
#[test]
fn test_istartswith_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["username".to_string()],
operator: WhereOperator::Istartswith,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("ILIKE"), "Istartswith must use ILIKE: {sql}");
assert_injection_safe(&sql, payload, "Istartswith operator");
}
}
#[test]
fn test_endswith_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["domain".to_string()],
operator: WhereOperator::Endswith,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("LIKE"), "Endswith must use LIKE: {sql}");
assert_injection_safe(&sql, payload, "Endswith operator");
}
}
#[test]
fn test_iendswith_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["domain".to_string()],
operator: WhereOperator::Iendswith,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("ILIKE"), "Iendswith must use ILIKE: {sql}");
assert_injection_safe(&sql, payload, "Iendswith operator");
}
}
#[test]
fn test_numeric_operators_injection_generates_safe_sql() {
let operators = vec![
(WhereOperator::Gt, ">"),
(WhereOperator::Gte, ">="),
(WhereOperator::Lt, "<"),
(WhereOperator::Lte, "<="),
];
for (op, expected_sql_op) in operators {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: op.clone(),
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains(expected_sql_op), "{op:?} must use {expected_sql_op}: {sql}");
assert_injection_safe(&sql, payload, &format!("{op:?} operator"));
}
}
}
#[test]
fn test_in_operator_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["status".to_string()],
operator: WhereOperator::In,
value: json!([payload, "safe_value"]),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("ARRAY["), "In must use ARRAY[]: {sql}");
assert_injection_safe(&sql, payload, "In operator");
}
}
#[test]
fn test_nin_operator_injection_generates_safe_sql() {
for payload in INJECTION_PAYLOADS {
let clause = WhereClause::Field {
path: vec!["status".to_string()],
operator: WhereOperator::Nin,
value: json!([payload, "safe_value"]),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("ARRAY["), "Nin must use ARRAY[]: {sql}");
assert_injection_safe(&sql, payload, "Nin operator");
}
}
#[test]
fn test_injection_in_simple_path_generates_safe_sql() {
let malicious_paths = vec![
"email'; DROP TABLE users; --",
"field' OR '1'='1",
"name'; DELETE FROM passwords; --",
];
for malicious_field in malicious_paths {
let clause = WhereClause::Field {
path: vec![malicious_field.to_string()],
operator: WhereOperator::Eq,
value: json!("safe_value"),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(
sql.contains("''"),
"Path injection: single quotes must be doubled in field name.\n field: {malicious_field}\n sql: {sql}"
);
assert!(sql.contains("= 'safe_value'"), "Value comparison must remain intact: {sql}");
}
}
#[test]
fn test_injection_in_nested_path_generates_safe_sql() {
let malicious_paths = vec![
vec!["user".to_string(), "email'; DROP TABLE--".to_string()],
vec!["data".to_string(), "'; DELETE FROM ".to_string()],
vec!["profile".to_string(), r#"" OR "=" "#.to_string()],
];
for path in malicious_paths {
let clause = WhereClause::Field {
path: path.clone(),
operator: WhereOperator::Eq,
value: json!("test@example.com"),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("data#>'{"), "Nested path must use JSON path syntax: {sql}");
let has_quote = path.iter().any(|s| s.contains('\''));
if has_quote {
assert!(
sql.contains("''"),
"Path component quotes must be doubled.\n path: {path:?}\n sql: {sql}"
);
}
}
}
#[test]
fn test_injection_in_and_clause_generates_safe_sql() {
let clause = WhereClause::And(vec![
WhereClause::Field {
path: vec!["email".to_string()],
operator: WhereOperator::Eq,
value: json!("'; DROP TABLE users; --"),
},
WhereClause::Field {
path: vec!["status".to_string()],
operator: WhereOperator::Eq,
value: json!("' OR '1'='1"),
},
]);
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("AND"), "Must produce AND clause: {sql}");
assert_injection_safe(&sql, "'; DROP TABLE users; --", "AND clause (first)");
assert_injection_safe(&sql, "' OR '1'='1", "AND clause (second)");
}
#[test]
fn test_injection_in_or_clause_generates_safe_sql() {
let clause = WhereClause::Or(vec![
WhereClause::Field {
path: vec!["name".to_string()],
operator: WhereOperator::Contains,
value: json!("' UNION SELECT * FROM passwords --"),
},
WhereClause::Field {
path: vec!["bio".to_string()],
operator: WhereOperator::Icontains,
value: json!("'; WAITFOR DELAY '00:00:05'--"),
},
]);
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("OR"), "Must produce OR clause: {sql}");
assert_injection_safe(&sql, "' UNION SELECT * FROM passwords --", "OR clause (first)");
}
#[test]
fn test_injection_in_not_clause_generates_safe_sql() {
let clause = WhereClause::Not(Box::new(WhereClause::Field {
path: vec!["test".to_string()],
operator: WhereOperator::Eq,
value: json!("'; DROP TABLE users; --"),
}));
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.starts_with("NOT ("), "Must produce NOT clause: {sql}");
assert_injection_safe(&sql, "'; DROP TABLE users; --", "NOT clause");
}
#[test]
fn test_null_byte_injection_generates_safe_sql() {
let clause = WhereClause::Field {
path: vec!["data".to_string()],
operator: WhereOperator::Eq,
value: json!("test\0attack"),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(!sql.is_empty(), "SQL must be generated");
}
#[test]
fn test_unicode_quote_injection_generates_safe_sql() {
let unicode_payloads = vec![
"'\u{2019}", "'\u{201C}", "\u{FB02}", ];
for payload in unicode_payloads {
let clause = WhereClause::Field {
path: vec!["text".to_string()],
operator: WhereOperator::Contains,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, payload, "Unicode quote injection");
}
}
#[test]
fn test_long_payload_injection_generates_safe_sql() {
let long_payload = "x".repeat(10000) + "' OR '1'='1";
let clause = WhereClause::Field {
path: vec!["comment".to_string()],
operator: WhereOperator::Contains,
value: json!(long_payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, &long_payload, "Long payload injection");
}
#[test]
fn test_encoded_payloads_generate_safe_sql() {
let encoded_payloads = vec![
"%27%20OR%20%271%27%3D%271", "0x27 OR 0x31=0x31", "0x3c7375622066696c653d7e20", ];
for payload in encoded_payloads {
let clause = WhereClause::Field {
path: vec!["data".to_string()],
operator: WhereOperator::Eq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains(payload), "Encoded payload must be preserved as literal: {sql}");
}
}
#[test]
fn test_backslash_escaping_generates_safe_sql() {
let backslash_payloads = vec![
"\\'; DROP TABLE users; --",
"\\'OR\\'1\\'=\\'1",
"admin\\' #",
];
for payload in backslash_payloads {
let clause = WhereClause::Field {
path: vec!["user".to_string()],
operator: WhereOperator::Eq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, payload, "Backslash escaping");
}
}
#[test]
fn test_comment_techniques_generate_safe_sql() {
let comment_payloads = vec!["'; --", "'; #", "'; /**/", "1' /*! UNION SELECT 1 */"];
for payload in comment_payloads {
let clause = WhereClause::Field {
path: vec!["id".to_string()],
operator: WhereOperator::Eq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, payload, "Comment technique");
}
}
#[test]
fn test_real_world_owasp_payloads_generate_safe_sql() {
let real_world_payloads = vec![
"' OR '1'='1' --",
"admin' --",
"' OR 1=1 --",
"' UNION ALL SELECT NULL,NULL,NULL --",
"' UNION SELECT table_name FROM information_schema.tables --",
"'; WAITFOR DELAY '00:00:05'--",
"' AND SLEEP(5) --",
"'; DROP TABLE users; --",
"'; INSERT INTO users VALUES(...); --",
"' AND '1'=('1",
"\\x27 OR \\x31=\\x31 --",
];
for payload in real_world_payloads {
let clause = WhereClause::Field {
path: vec!["vulnerable_field".to_string()],
operator: WhereOperator::Eq,
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_injection_safe(&sql, payload, "OWASP payload");
}
}
#[test]
fn test_all_supported_operators_generate_safe_sql_for_injection() {
let operators: Vec<(WhereOperator, &str)> = vec![
(WhereOperator::Eq, "="),
(WhereOperator::Neq, "!="),
(WhereOperator::Gt, ">"),
(WhereOperator::Gte, ">="),
(WhereOperator::Lt, "<"),
(WhereOperator::Lte, "<="),
(WhereOperator::Contains, "LIKE"),
(WhereOperator::Icontains, "ILIKE"),
(WhereOperator::Startswith, "LIKE"),
(WhereOperator::Istartswith, "ILIKE"),
(WhereOperator::Endswith, "LIKE"),
(WhereOperator::Iendswith, "ILIKE"),
];
let payload = "'; DROP TABLE users; --";
for (op, expected_sql_op) in operators {
let clause = WhereClause::Field {
path: vec!["field".to_string()],
operator: op.clone(),
value: json!(payload),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(
sql.contains(expected_sql_op),
"{op:?}: expected SQL operator {expected_sql_op} in: {sql}"
);
assert_injection_safe(&sql, payload, &format!("{op:?}"));
}
}
#[test]
fn test_is_null_operator_ignores_value_payload() {
let clause = WhereClause::Field {
path: vec!["deleted_at".to_string()],
operator: WhereOperator::IsNull,
value: json!(true),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert_eq!(sql, "data->>'deleted_at' IS NULL");
let clause = WhereClause::Field {
path: vec!["deleted_at".to_string()],
operator: WhereOperator::IsNull,
value: json!("'; DROP TABLE users; --"),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(!sql.contains("DROP"), "IsNull must not include value payload in SQL: {sql}");
}
#[test]
fn test_array_operator_injection_generates_safe_sql() {
let clause = WhereClause::Field {
path: vec!["tags".to_string()],
operator: WhereOperator::ArrayContains,
value: json!(["normal", "'; DROP TABLE users; --"]),
};
let sql = WhereSqlGenerator::to_sql(&clause).unwrap();
assert!(sql.contains("::jsonb"), "Must produce JSONB cast: {sql}");
assert!(sql.contains("''"), "Quotes inside JSON array values must be doubled: {sql}");
}
#[test]
fn test_empty_boolean_clauses_generate_safe_sql() {
let and_sql = WhereSqlGenerator::to_sql(&WhereClause::And(vec![])).unwrap();
assert_eq!(and_sql, "TRUE");
let or_sql = WhereSqlGenerator::to_sql(&WhereClause::Or(vec![])).unwrap();
assert_eq!(or_sql, "FALSE");
}