#![allow(clippy::format_push_string)] #![allow(clippy::default_trait_access)] use fraiseql_core::db::path_escape;
#[test]
fn test_postgres_path_with_single_quote() {
let segment = "user'; DROP TABLE users; --";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "user''; DROP TABLE users; --");
let sql = format!("data->>'{}'", escaped);
assert!(sql.contains("data->>'user''"), "Escaping structure broken");
}
#[test]
fn test_postgres_path_with_multiple_quotes() {
let segment = "it's a test' and '1'='1";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
let quote_count = segment.matches('\'').count();
let escaped_quote_count = escaped.matches("''").count();
assert_eq!(quote_count, escaped_quote_count, "Not all quotes were properly escaped");
}
#[test]
fn test_postgres_path_with_sql_keywords() {
let vectors = vec![
"DELETE FROM users",
"DROP TABLE users",
"UPDATE users SET",
"INSERT INTO users",
"SELECT * FROM",
];
for keyword_path in vectors {
let escaped = path_escape::escape_postgres_jsonb_segment(keyword_path);
let sql = format!("data->'{}'", escaped);
assert!(sql.contains(keyword_path), "Path lost during escaping");
}
}
#[test]
fn test_postgres_path_with_brackets() {
let segment = "field'][0";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "field''][0");
let sql = format!("data->'{}'", escaped);
assert!(sql.contains("data->'field''"), "Quote escaping not applied");
}
#[test]
fn test_postgres_multipart_path_injection() {
let path = vec![
"user'; DROP--".to_string(),
"admin' OR '1'='1".to_string(),
"test".to_string(),
];
let escaped = path_escape::escape_postgres_jsonb_path(&path);
assert_eq!(escaped[0], "user''; DROP--");
assert_eq!(escaped[1], "admin'' OR ''1''=''1");
assert_eq!(escaped[2], "test");
let mut sql = "data".to_string();
for (i, segment) in escaped.iter().enumerate() {
if i < escaped.len() - 1 {
sql.push_str(&format!("->'{}'", segment));
} else {
sql.push_str(&format!("->>'{}' ", segment));
}
}
assert!(sql.contains("data->"), "SQL structure broken");
assert!(sql.contains("user''"), "Quote escaping not applied");
}
#[test]
fn test_postgres_empty_path_segment() {
let segment = "";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "");
}
#[test]
fn test_postgres_unicode_in_path() {
let segment = "user' UNION SELECT 'ä½ å¥½";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "user'' UNION SELECT ''ä½ å¥½");
let sql = format!("data->'{}'", escaped);
assert!(sql.contains("data->'user''"), "Quote escaping failed");
}
#[test]
fn test_postgres_only_quotes() {
let segment = "''''";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "''''''''");
}
#[test]
fn test_mysql_path_with_single_quote() {
let path = vec!["user'; DROP TABLE users; --".to_string()];
let escaped = path_escape::escape_mysql_json_path(&path);
assert!(escaped.contains("''"), "Single quote not escaped for MySQL");
assert_eq!(escaped, "$.user''; DROP TABLE users; --");
}
#[test]
fn test_mysql_multipart_path_with_injection() {
let path = vec!["user'".to_string(), "admin' OR '1'='1".to_string()];
let escaped = path_escape::escape_mysql_json_path(&path);
assert!(escaped.contains("''"), "Quotes not properly escaped");
}
#[test]
fn test_mysql_path_with_sql_keywords() {
let vectors = vec![
vec!["DELETE FROM users".to_string()],
vec!["DROP TABLE".to_string()],
vec!["UPDATE users".to_string()],
];
for path in vectors {
let escaped = path_escape::escape_mysql_json_path(&path);
assert!(escaped.starts_with("$."), "JSON path must start with $.");
assert!(
escaped.contains("DELETE") || escaped.contains("DROP") || escaped.contains("UPDATE"),
"Keywords should be preserved"
);
}
}
#[test]
fn test_mysql_preserves_dot_notation() {
let path = vec![
"user".to_string(),
"profile".to_string(),
"name".to_string(),
];
let escaped = path_escape::escape_mysql_json_path(&path);
assert_eq!(escaped, "$.user.profile.name");
}
#[test]
fn test_sqlite_path_with_single_quote() {
let path = vec!["user'; DROP TABLE users; --".to_string()];
let escaped = path_escape::escape_sqlite_json_path(&path);
assert!(escaped.contains("''"), "Single quote not escaped for SQLite");
assert_eq!(escaped, "$.user''; DROP TABLE users; --");
}
#[test]
fn test_sqlite_multipart_path() {
let path = vec![
"user'".to_string(),
"data".to_string(),
"admin' OR '1'='1".to_string(),
];
let escaped = path_escape::escape_sqlite_json_path(&path);
let sql = format!("json_extract(data, '{}')", escaped);
assert!(!sql.contains("OR '1'='1"), "Boolean-based injection not prevented");
}
#[test]
fn test_sqlserver_path_with_single_quote() {
let path = vec!["user'; DROP TABLE users; --".to_string()];
let escaped = path_escape::escape_sqlserver_json_path(&path);
assert!(escaped.contains("''"), "Single quote not escaped for SQL Server");
assert_eq!(escaped, "$.user''; DROP TABLE users; --");
}
#[test]
fn test_sqlserver_multipart_path() {
let path = vec!["user'".to_string(), "admin' OR '1'='1".to_string()];
let escaped = path_escape::escape_sqlserver_json_path(&path);
let sql = format!("JSON_VALUE(data, '{}')", escaped);
assert!(sql.contains("user''"), "Single quotes not doubled");
assert!(sql.contains("admin''"), "Single quotes not doubled");
}
#[test]
fn test_all_databases_with_null_byte() {
let segment_with_null = "test\x00injection";
let pg_escaped = path_escape::escape_postgres_jsonb_segment(segment_with_null);
let mysql_escaped = path_escape::escape_mysql_json_path(&[segment_with_null.to_string()]);
let sqlite_escaped = path_escape::escape_sqlite_json_path(&[segment_with_null.to_string()]);
let sqlserver_escaped =
path_escape::escape_sqlserver_json_path(&[segment_with_null.to_string()]);
assert!(pg_escaped.contains("test"), "Null byte caused truncation in PostgreSQL");
assert!(mysql_escaped.contains("test"), "Null byte caused truncation in MySQL");
assert!(sqlite_escaped.contains("test"), "Null byte caused truncation in SQLite");
assert!(sqlserver_escaped.contains("test"), "Null byte caused truncation in SQL Server");
}
#[test]
fn test_all_databases_with_very_long_path() {
let mut long_path = Vec::new();
for i in 0..50 {
long_path.push(format!("segment_{}', DROP TABLE users;--", i));
}
let pg_escaped = path_escape::escape_postgres_jsonb_path(&long_path);
assert_eq!(pg_escaped.len(), 50, "Path segments lost during escaping");
for (i, segment) in pg_escaped.iter().enumerate() {
assert!(segment.contains("segment_"), "Segment identifier lost");
if segment.contains('\'') {
assert!(segment.contains("''"), "Quote not doubled in segment {}", i);
}
}
}
#[test]
fn test_path_that_looks_like_json_syntax() {
let attack_vectors = vec![
"field'}]",
"[0]",
"{\"key\": \"value\"}",
"\\u0000",
"\\n\\r\\t",
];
for vector in attack_vectors {
let escaped_pg = path_escape::escape_postgres_jsonb_segment(vector);
let _escaped_mysql = path_escape::escape_mysql_json_path(&[vector.to_string()]);
let pg_sql = format!("data->'{}'", escaped_pg);
assert!(pg_sql.contains("data->"), "SQL structure broken");
assert!(pg_sql.contains('\''), "Quoting broken");
}
}
#[test]
fn test_real_world_sql_injection_patterns() {
let real_attacks = vec![
"admin' --",
"' OR '1'='1",
"'; DELETE FROM users; --",
"1' UNION SELECT * FROM users --",
"' OR 1=1 --",
"admin'/*",
"' or 1 like '1",
"1' AND SLEEP(5) --",
"' OR 'a'='a",
];
for attack in real_attacks {
let pg_escaped = path_escape::escape_postgres_jsonb_segment(attack);
let mysql_escaped = path_escape::escape_mysql_json_path(&[attack.to_string()]);
let pg_sql = format!("WHERE data->'{}'", pg_escaped);
let mysql_sql = format!("WHERE JSON_EXTRACT(data, '{}')", mysql_escaped);
assert!(pg_sql.contains("WHERE data->"), "PostgreSQL wrapper broken");
assert!(mysql_sql.contains("WHERE JSON_EXTRACT"), "MySQL wrapper broken");
}
}
#[test]
fn test_postgres_escaping_idempotency() {
let original = "user'name";
let once = path_escape::escape_postgres_jsonb_segment(original);
assert_eq!(once, "user''name");
let twice = path_escape::escape_postgres_jsonb_segment(&once);
assert_eq!(twice, "user''''name");
}
#[test]
fn test_backslash_not_special_in_postgres() {
let segment = "field\\path";
let escaped = path_escape::escape_postgres_jsonb_segment(segment);
assert_eq!(escaped, "field\\path");
}
#[test]
fn test_all_special_chars_except_quotes() {
let special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?/~`";
let pg_escaped = path_escape::escape_postgres_jsonb_segment(special_chars);
let mysql_escaped = path_escape::escape_mysql_json_path(&[special_chars.to_string()]);
let sqlite_escaped = path_escape::escape_sqlite_json_path(&[special_chars.to_string()]);
let sqlserver_escaped = path_escape::escape_sqlserver_json_path(&[special_chars.to_string()]);
assert!(pg_escaped.contains("!@#$%^&*"), "Special chars lost in PostgreSQL");
assert!(mysql_escaped.contains("!@#$%^&*"), "Special chars lost in MySQL");
assert!(sqlite_escaped.contains("!@#$%^&*"), "Special chars lost in SQLite");
assert!(sqlserver_escaped.contains("!@#$%^&*"), "Special chars lost in SQL Server");
}
#[test]
fn test_various_quote_positions() {
let vectors = vec![
"'leading",
"trailing'",
"mid'dle",
"mul'ti'ple'quotes",
"''consecutive",
];
for vector in vectors {
let pg_escaped = path_escape::escape_postgres_jsonb_segment(vector);
let single_quotes = vector.matches('\'').count();
let doubled_quotes = pg_escaped.matches("''").count();
assert_eq!(single_quotes, doubled_quotes, "Quote escaping failed for: {}", vector);
}
}
#[test]
fn test_mysql_preserves_structure_with_quotes() {
let path = vec!["user'name".to_string(), "email".to_string()];
let escaped = path_escape::escape_mysql_json_path(&path);
assert!(escaped.starts_with("$."), "JSON path must start with $.");
assert!(escaped.contains("user''name"), "Quote should be escaped with doubled quotes");
assert!(escaped.contains("email"), "Second segment should be present");
}