sz_orm_graph/
validator.rs1use crate::error::GraphError;
4use crate::query::CypherQuery;
5
6const SQL_KEYWORDS: &[&str] = &[
8 "SELECT ",
9 "INSERT ",
10 "UPDATE ",
11 "DELETE ",
12 "CREATE TABLE ",
13 "DROP TABLE ",
14 "ALTER TABLE ",
15 "select ",
16 "insert ",
17 "update ",
18 "delete ",
19 "create table ",
20 "drop table ",
21 "alter table ",
22];
23
24pub struct CypherValidator;
26
27impl CypherValidator {
28 pub fn validate(query: &CypherQuery) -> Result<(), GraphError> {
30 Self::check_sql_keywords(&query.cypher)?;
31 Self::check_parameterization(&query.cypher)?;
32 Ok(())
33 }
34
35 fn check_sql_keywords(cypher: &str) -> Result<(), GraphError> {
37 for keyword in SQL_KEYWORDS {
38 if cypher.contains(keyword) {
39 return Err(GraphError::SqlNotSupported(format!(
40 "SQL keyword '{}' detected in Cypher query",
41 keyword.trim()
42 )));
43 }
44 }
45 Ok(())
46 }
47
48 fn check_parameterization(cypher: &str) -> Result<(), GraphError> {
52 let mut in_string = false;
53 let mut quote_char = '\0';
54 let mut string_start = 0usize;
55 let chars: Vec<char> = cypher.chars().collect();
56
57 for (i, &ch) in chars.iter().enumerate() {
58 if matches!(ch, '\'' | '"') {
59 if !in_string {
60 in_string = true;
61 quote_char = ch;
62 string_start = i;
63 } else if ch == quote_char {
64 let literal = &cypher[string_start + 1..i];
65 if literal.len() > 2 && !literal.starts_with('$') {
66 return Err(GraphError::ParameterizationError(format!(
67 "string literal '{}' should use parameterized form ($param)",
68 literal
69 )));
70 }
71 in_string = false;
72 quote_char = '\0';
73 }
74 }
75 }
76
77 if in_string {
78 return Err(GraphError::ParameterizationError(
79 "unterminated string literal".into(),
80 ));
81 }
82
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::collections::HashMap;
91
92 #[test]
93 fn test_reject_sql_select() {
94 let q = CypherQuery::new("SELECT * FROM nodes");
95 let result = CypherValidator::validate(&q);
96 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
97 }
98
99 #[test]
100 fn test_reject_sql_insert() {
101 let q = CypherQuery::new("INSERT INTO nodes VALUES (1)");
102 let result = CypherValidator::validate(&q);
103 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
104 }
105
106 #[test]
107 fn test_reject_sql_create_table() {
108 let q = CypherQuery::new("CREATE TABLE foo (id INT)");
109 let result = CypherValidator::validate(&q);
110 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
111 }
112
113 #[test]
114 fn test_accept_parameterized_cypher() {
115 let q = CypherQuery::new("MATCH (n:Person {name: $name}) RETURN n");
116 let result = CypherValidator::validate(&q);
117 assert!(result.is_ok());
118 }
119
120 #[test]
121 fn test_reject_string_literal() {
122 let q = CypherQuery::new("MATCH (n:Person {name: 'Alice'}) RETURN n");
123 let result = CypherValidator::validate(&q);
124 assert!(matches!(result, Err(GraphError::ParameterizationError(_))));
125 }
126
127 #[test]
128 fn test_injection_as_parameter() {
129 let mut params = HashMap::new();
130 params.insert("name".to_string(), serde_json::json!("' OR 1=1 --"));
131 let q = CypherQuery::with_params("MATCH (n:Person {name: $name}) RETURN n", params);
132 let result = CypherValidator::validate(&q);
133 assert!(result.is_ok());
134 }
135}