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 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}