Skip to main content

sz_orm_core/
sql_safety.rs

1//! SQL 安全工具:标识符与外键动作校验
2//!
3//! v0.2.2 引入:为 `phinx_migration` / `migration` / `data_permission` 等模块提供
4//! 统一的 SQL 注入防护原语。所有需要拼接 SQL 标识符(表名/列名/约束名/索引名)
5//! 的位置必须先经 `validate_identifier` 校验;所有外键 ON DELETE / ON UPDATE
6//! 动作必须经 `validate_fk_action` 校验。
7
8use crate::error::DbError;
9
10/// 校验 SQL 标识符(表名/列名/约束名/索引名)
11///
12/// 仅允许 ASCII 字母数字 + 下划线,不以数字开头,长度 1-63(PostgreSQL 限制)。
13/// 拒绝任何 SQL 元字符(引号、分号、空格、注释、引号转义等),杜绝 SQL 注入。
14pub fn validate_identifier(name: &str, kind: &str) -> Result<(), DbError> {
15    if name.is_empty() || name.len() > 63 {
16        return Err(DbError::InvalidInput(format!(
17            "invalid {}: empty or too long (max 63 chars): {:?}",
18            kind, name
19        )));
20    }
21    let mut chars = name.chars();
22    let first = chars.next().expect("non-empty checked above");
23    if !first.is_ascii_alphabetic() && first != '_' {
24        return Err(DbError::InvalidInput(format!(
25            "invalid {}: must start with ASCII letter or underscore, got {:?}",
26            kind, name
27        )));
28    }
29    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
30        return Err(DbError::InvalidInput(format!(
31            "invalid {}: only ASCII alphanumeric and underscore allowed, got {:?}",
32            kind, name
33        )));
34    }
35    Ok(())
36}
37
38/// 校验外键 ON DELETE / ON UPDATE 动作
39///
40/// 仅允许标准 SQL 动作(大小写不敏感):CASCADE / SET NULL / SET DEFAULT /
41/// RESTRICT / NO ACTION。拒绝任何其他字符串,防止通过自定义动作注入 SQL。
42pub fn validate_fk_action(action: &str) -> Result<(), DbError> {
43    const ALLOWED: &[&str] = &[
44        "CASCADE",
45        "SET NULL",
46        "SET DEFAULT",
47        "RESTRICT",
48        "NO ACTION",
49    ];
50    let upper = action.trim().to_uppercase();
51    if !ALLOWED.contains(&upper.as_str()) {
52        return Err(DbError::InvalidInput(format!(
53            "invalid foreign key action: {:?}, allowed: {:?}",
54            action, ALLOWED
55        )));
56    }
57    Ok(())
58}
59
60/// 校验 IN 子句中的 id 值
61///
62/// 用于 `WHERE id IN (...)` 中的元素值。允许:
63/// - 纯数字(如 "1", "100")
64/// - 字母数字+下划线+减号(如 "abc", "user_123", "uuid-abc")
65/// - 长度 1-128
66///
67/// 拒绝任何 SQL 元字符(引号、分号、空格、注释、括号等)和 `--` 注释序列,杜绝 SQL 注入。
68pub fn validate_id_value(id: &str) -> Result<(), DbError> {
69    if id.is_empty() || id.len() > 128 {
70        return Err(DbError::InvalidInput(format!(
71            "invalid id value: empty or too long (max 128 chars): {:?}",
72            id
73        )));
74    }
75    // 显式拒绝 SQL 行注释序列(即使 - 是允许字符,-- 仍是 SQL 注释)
76    if id.contains("--") {
77        return Err(DbError::InvalidInput(format!(
78            "invalid id value: SQL comment sequence '--' not allowed, got {:?}",
79            id
80        )));
81    }
82    if !id
83        .chars()
84        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
85    {
86        return Err(DbError::InvalidInput(format!(
87            "invalid id value: only ASCII alphanumeric, underscore and hyphen allowed, got {:?}",
88            id
89        )));
90    }
91    Ok(())
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_validate_identifier_valid() {
100        assert!(validate_identifier("users", "table").is_ok());
101        assert!(validate_identifier("_idx", "index").is_ok());
102        assert!(validate_identifier("geom_2026", "column").is_ok());
103        assert!(validate_identifier("a", "column").is_ok());
104        assert!(validate_identifier(&"a".repeat(63), "table").is_ok());
105    }
106
107    #[test]
108    fn test_validate_identifier_injection_attempts() {
109        // 经典 SQL 注入尝试
110        assert!(validate_identifier("users; DROP TABLE users", "table").is_err());
111        assert!(validate_identifier("col'--", "column").is_err());
112        assert!(validate_identifier("col\"x", "column").is_err());
113        assert!(validate_identifier("col`x", "column").is_err());
114        assert!(validate_identifier("col--", "column").is_err());
115        assert!(validate_identifier("col/*x*/", "column").is_err());
116        assert!(validate_identifier("col OR 1=1", "column").is_err());
117        // 数字开头
118        assert!(validate_identifier("1col", "column").is_err());
119        // 空字符串
120        assert!(validate_identifier("", "table").is_err());
121        // 过长
122        let long_name = "a".repeat(64);
123        assert!(validate_identifier(&long_name, "table").is_err());
124        // 含空格
125        assert!(validate_identifier("col name", "column").is_err());
126        // 含特殊字符
127        assert!(validate_identifier("col$name", "column").is_err());
128        assert!(validate_identifier("col%name", "column").is_err());
129        assert!(validate_identifier("col@name", "column").is_err());
130    }
131
132    #[test]
133    fn test_validate_fk_action_valid() {
134        assert!(validate_fk_action("CASCADE").is_ok());
135        assert!(validate_fk_action("cascade").is_ok()); // 大小写不敏感
136        assert!(validate_fk_action("Cascade").is_ok());
137        assert!(validate_fk_action("SET NULL").is_ok());
138        assert!(validate_fk_action("set null").is_ok());
139        assert!(validate_fk_action("SET DEFAULT").is_ok());
140        assert!(validate_fk_action("RESTRICT").is_ok());
141        assert!(validate_fk_action("NO ACTION").is_ok());
142        assert!(validate_fk_action("  NO ACTION  ").is_ok()); // 容许前后空白
143    }
144
145    #[test]
146    fn test_validate_fk_action_injection_attempts() {
147        assert!(validate_fk_action("CASCADE; DROP TABLE users").is_err());
148        assert!(validate_fk_action("CASCADE--").is_err());
149        assert!(validate_fk_action("CASCADE OR 1=1").is_err());
150        assert!(validate_fk_action("EVIL").is_err());
151        assert!(validate_fk_action("' OR '1'='1").is_err());
152        assert!(validate_fk_action("").is_err());
153    }
154
155    #[test]
156    fn test_validate_id_value_valid() {
157        assert!(validate_id_value("1").is_ok());
158        assert!(validate_id_value("100").is_ok());
159        assert!(validate_id_value("abc").is_ok());
160        assert!(validate_id_value("user_123").is_ok());
161        assert!(validate_id_value("uuid-abc-123").is_ok());
162        assert!(validate_id_value(&"a".repeat(128)).is_ok());
163    }
164
165    #[test]
166    fn test_validate_id_value_injection_attempts() {
167        // 经典 SQL 注入
168        assert!(validate_id_value("1; DROP TABLE users").is_err());
169        assert!(validate_id_value("1) OR 1=1").is_err());
170        assert!(validate_id_value("' OR '1'='1").is_err());
171        assert!(validate_id_value("1--").is_err());
172        assert!(validate_id_value("1/*comment*/").is_err());
173        assert!(validate_id_value("1;").is_err());
174        assert!(validate_id_value("1'").is_err());
175        assert!(validate_id_value("1\"").is_err());
176        // 空字符串
177        assert!(validate_id_value("").is_err());
178        // 过长
179        let long_id = "a".repeat(129);
180        assert!(validate_id_value(&long_id).is_err());
181        // 含空格
182        assert!(validate_id_value("1 2").is_err());
183        // 含点号(避免列名引用)
184        assert!(validate_id_value("users.id").is_err());
185    }
186}