Skip to main content

sz_orm_graph/
validator.rs

1//! # Validator — Cypher 参数化校验 + SQL 透传拒绝
2
3use crate::error::GraphError;
4use crate::query::CypherQuery;
5
6/// SQL 关键字列表(图接口拒绝 SQL 透传)
7const 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
24/// Cypher 查询校验器
25pub struct CypherValidator;
26
27impl CypherValidator {
28    /// 校验查询:拒绝 SQL 透传 + 强制参数化
29    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    /// 检测 SQL 关键字 → 返回 GraphError::SqlNotSupported
36    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    /// 检测字面量拼接 → 返回 GraphError::ParameterizationError
49    ///
50    /// 检测规则:Cypher 查询中字符串字面量(单引号或双引号包裹)且非参数占位符($param)
51    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}