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 string_start = 0usize;
54 let chars: Vec<char> = cypher.chars().collect();
55
56 for (i, &ch) in chars.iter().enumerate() {
57 if ch == '\'' {
58 if !in_string {
59 in_string = true;
60 string_start = i;
61 } else {
62 let literal = &cypher[string_start + 1..i];
63 if literal.len() > 2 && !literal.starts_with('$') {
64 return Err(GraphError::ParameterizationError(format!(
65 "string literal '{}' should use parameterized form ($param)",
66 literal
67 )));
68 }
69 in_string = false;
70 }
71 }
72 }
73
74 if in_string {
75 return Err(GraphError::ParameterizationError(
76 "unterminated string literal".into(),
77 ));
78 }
79
80 Ok(())
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::collections::HashMap;
88
89 #[test]
90 fn test_reject_sql_select() {
91 let q = CypherQuery::new("SELECT * FROM nodes");
92 let result = CypherValidator::validate(&q);
93 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
94 }
95
96 #[test]
97 fn test_reject_sql_insert() {
98 let q = CypherQuery::new("INSERT INTO nodes VALUES (1)");
99 let result = CypherValidator::validate(&q);
100 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
101 }
102
103 #[test]
104 fn test_reject_sql_create_table() {
105 let q = CypherQuery::new("CREATE TABLE foo (id INT)");
106 let result = CypherValidator::validate(&q);
107 assert!(matches!(result, Err(GraphError::SqlNotSupported(_))));
108 }
109
110 #[test]
111 fn test_accept_parameterized_cypher() {
112 let q = CypherQuery::new("MATCH (n:Person {name: $name}) RETURN n");
113 let result = CypherValidator::validate(&q);
114 assert!(result.is_ok());
115 }
116
117 #[test]
118 fn test_reject_string_literal() {
119 let q = CypherQuery::new("MATCH (n:Person {name: 'Alice'}) RETURN n");
120 let result = CypherValidator::validate(&q);
121 assert!(matches!(result, Err(GraphError::ParameterizationError(_))));
122 }
123
124 #[test]
125 fn test_injection_as_parameter() {
126 let mut params = HashMap::new();
127 params.insert("name".to_string(), serde_json::json!("' OR 1=1 --"));
128 let q = CypherQuery::with_params("MATCH (n:Person {name: $name}) RETURN n", params);
129 let result = CypherValidator::validate(&q);
130 assert!(result.is_ok());
131 }
132}