Skip to main content

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