mik_sql/validate/
expression.rs1#[inline]
52#[must_use]
53pub fn is_valid_sql_expression(s: &str) -> bool {
54 if s.is_empty() || s.len() > 1000 {
56 return false;
57 }
58
59 if s.contains("--") || s.contains("/*") || s.contains("*/") {
61 return false;
62 }
63
64 if s.contains(';') {
66 return false;
67 }
68
69 if s.contains('`') {
71 return false;
72 }
73
74 let lower = s.to_ascii_lowercase();
76
77 const DANGEROUS_KEYWORDS: &[&str] = &[
79 "select",
81 "insert",
82 "update",
83 "delete",
84 "drop",
85 "truncate",
86 "alter",
87 "create",
88 "grant",
89 "revoke",
90 "exec",
91 "execute",
92 "union",
93 "into",
94 "from",
95 "where",
96 "having",
97 "group",
98 "order",
99 "limit",
100 "offset",
101 "fetch",
102 "returning",
103 "sleep",
105 "benchmark",
106 "waitfor",
107 "pg_sleep",
108 "dbms_lock",
109 "load_file",
111 "into_outfile",
112 "into_dumpfile",
113 "chr",
115 "char",
116 "ascii",
117 "unicode",
118 "hex",
119 "unhex",
120 "convert",
121 "cast",
122 "encode",
123 "decode",
124 ];
125
126 for keyword in DANGEROUS_KEYWORDS {
127 if contains_sql_keyword(&lower, keyword) {
128 return false;
129 }
130 }
131
132 if lower.contains("pg_")
134 || lower.contains("sqlite_")
135 || lower.contains("information_schema")
136 || lower.contains("sys.")
137 {
138 return false;
139 }
140
141 if lower.contains("0x") || lower.contains("\\x") {
143 return false;
144 }
145
146 true
147}
148
149#[inline]
153fn contains_sql_keyword(haystack: &str, keyword: &str) -> bool {
154 let bytes = haystack.as_bytes();
155 let kw_bytes = keyword.as_bytes();
156 let kw_len = kw_bytes.len();
157
158 if kw_len == 0 || bytes.len() < kw_len {
159 return false;
160 }
161
162 for i in 0..=(bytes.len() - kw_len) {
163 if &bytes[i..i + kw_len] == kw_bytes {
165 let before_ok =
167 i == 0 || (!bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_');
168 let after_ok = i + kw_len == bytes.len()
169 || (!bytes[i + kw_len].is_ascii_alphanumeric() && bytes[i + kw_len] != b'_');
170
171 if before_ok && after_ok {
172 return true;
173 }
174 }
175 }
176
177 false
178}
179
180#[inline]
186pub fn assert_valid_sql_expression(s: &str, context: &str) {
187 assert!(
188 is_valid_sql_expression(s),
189 "Invalid SQL expression for {context}: '{s}' contains dangerous patterns \
190 (comments, semicolons, or SQL keywords)"
191 );
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_valid_sql_expressions() {
200 assert!(is_valid_sql_expression("first_name || ' ' || last_name"));
202 assert!(is_valid_sql_expression("quantity * price"));
203 assert!(is_valid_sql_expression("COALESCE(nickname, name)"));
204 assert!(is_valid_sql_expression("age + 1"));
205 assert!(is_valid_sql_expression("CASE WHEN x > 0 THEN y ELSE z END"));
206 assert!(is_valid_sql_expression("price * 1.1"));
207 assert!(is_valid_sql_expression("UPPER(name)"));
208 assert!(is_valid_sql_expression("LENGTH(description)"));
209
210 assert!(is_valid_sql_expression("last_updated")); assert!(is_valid_sql_expression("created_at")); assert!(is_valid_sql_expression("selected_items")); assert!(is_valid_sql_expression("deleted_at")); assert!(is_valid_sql_expression("order_total")); assert!(is_valid_sql_expression("group_name")); assert!(is_valid_sql_expression("from_date")); assert!(is_valid_sql_expression("where_clause")); }
220
221 #[test]
222 fn test_invalid_sql_expressions() {
223 assert!(!is_valid_sql_expression(""));
225
226 assert!(!is_valid_sql_expression("name -- comment"));
228 assert!(!is_valid_sql_expression("/* comment */ name"));
229 assert!(!is_valid_sql_expression("name */ attack"));
230
231 assert!(!is_valid_sql_expression("1; DROP TABLE users"));
233 assert!(!is_valid_sql_expression("name;"));
234
235 assert!(!is_valid_sql_expression("`table`"));
237
238 assert!(!is_valid_sql_expression("(SELECT password)"));
240 assert!(!is_valid_sql_expression("INSERT INTO x"));
241 assert!(!is_valid_sql_expression("DELETE FROM x"));
242 assert!(!is_valid_sql_expression("DROP TABLE x"));
243 assert!(!is_valid_sql_expression("UPDATE SET y=1"));
244 assert!(!is_valid_sql_expression("UNION ALL"));
245 assert!(!is_valid_sql_expression("x FROM y"));
246 assert!(!is_valid_sql_expression("x WHERE y"));
247
248 assert!(!is_valid_sql_expression("pg_catalog.pg_tables"));
250 assert!(!is_valid_sql_expression("sqlite_master"));
251 assert!(!is_valid_sql_expression("information_schema.tables"));
252
253 assert!(!is_valid_sql_expression("0x48454C4C4F"));
255 assert!(!is_valid_sql_expression("\\x48454C4C4F"));
256
257 assert!(!is_valid_sql_expression("SLEEP(10)"));
259 assert!(!is_valid_sql_expression("pg_sleep(5)"));
260 assert!(!is_valid_sql_expression("BENCHMARK(1000000, SHA1('test'))"));
261 assert!(!is_valid_sql_expression("WAITFOR DELAY '0:0:5'"));
262
263 assert!(!is_valid_sql_expression("LOAD_FILE('/etc/passwd')"));
265 }
266
267 #[test]
268 #[should_panic(expected = "Invalid SQL expression")]
269 fn test_assert_valid_expression_panics() {
270 assert_valid_sql_expression("1; DROP TABLE users", "computed field");
271 }
272
273 #[test]
278 fn test_sqli_classic_or_true() {
279 assert!(!is_valid_sql_expression("' OR 1=1--")); assert!(!is_valid_sql_expression("1; OR 1=1")); }
283
284 #[test]
285 fn test_sqli_drop_table() {
286 assert!(!is_valid_sql_expression("'; DROP TABLE users--"));
288 assert!(!is_valid_sql_expression("'; DROP TABLE users;--"));
289 assert!(!is_valid_sql_expression("1; DROP TABLE users"));
290 assert!(!is_valid_sql_expression("DROP TABLE users"));
291 assert!(!is_valid_sql_expression("drop table users"));
292 assert!(!is_valid_sql_expression("DrOp TaBlE users"));
293 }
294
295 #[test]
296 fn test_sqli_union_attacks() {
297 assert!(!is_valid_sql_expression("' UNION SELECT * FROM users--"));
299 assert!(!is_valid_sql_expression(
300 "' UNION ALL SELECT password FROM users--"
301 ));
302 assert!(!is_valid_sql_expression("1 UNION SELECT 1,2,3"));
303 assert!(!is_valid_sql_expression(
304 "UNION SELECT username,password FROM admin"
305 ));
306 assert!(!is_valid_sql_expression("' union select null,null,null--"));
307 }
308
309 #[test]
310 fn test_sqli_comment_injection() {
311 assert!(!is_valid_sql_expression("admin'--")); assert!(!is_valid_sql_expression("admin'/*")); assert!(!is_valid_sql_expression("*/; DROP TABLE users--")); assert!(!is_valid_sql_expression("1/**/OR/**/1=1")); }
317
318 #[test]
319 fn test_sqli_stacked_queries() {
320 assert!(!is_valid_sql_expression(
322 "; INSERT INTO users VALUES('hacker')"
323 ));
324 assert!(!is_valid_sql_expression("; UPDATE users SET role='admin'"));
325 assert!(!is_valid_sql_expression("; DELETE FROM users"));
326 assert!(!is_valid_sql_expression("1; SELECT * FROM passwords"));
327 assert!(!is_valid_sql_expression("'; TRUNCATE TABLE logs;--"));
328 }
329
330 #[test]
331 fn test_sqli_time_based_blind() {
332 assert!(!is_valid_sql_expression("SLEEP(5)"));
334 assert!(!is_valid_sql_expression("1 AND SLEEP(5)"));
335 assert!(!is_valid_sql_expression("pg_sleep(5)"));
336 assert!(!is_valid_sql_expression("1; SELECT pg_sleep(10)"));
337 assert!(!is_valid_sql_expression("BENCHMARK(10000000,SHA1('test'))"));
338 assert!(!is_valid_sql_expression("WAITFOR DELAY '0:0:5'"));
339 assert!(!is_valid_sql_expression("dbms_lock.sleep(5)"));
340 }
341
342 #[test]
343 fn test_sqli_file_operations() {
344 assert!(!is_valid_sql_expression("LOAD_FILE('/etc/passwd')"));
346 assert!(!is_valid_sql_expression("load_file('/etc/shadow')"));
347 assert!(!is_valid_sql_expression(
348 "INTO OUTFILE '/var/www/shell.php'"
349 ));
350 assert!(!is_valid_sql_expression("INTO DUMPFILE '/tmp/data'"));
351 assert!(!is_valid_sql_expression("into_outfile('/tmp/x')"));
352 assert!(!is_valid_sql_expression("into_dumpfile('/tmp/x')"));
353 }
354
355 #[test]
356 fn test_sqli_system_catalog_access() {
357 assert!(!is_valid_sql_expression("pg_tables"));
359 assert!(!is_valid_sql_expression("pg_catalog.pg_tables"));
360 assert!(!is_valid_sql_expression("sqlite_master"));
361 assert!(!is_valid_sql_expression("information_schema.tables"));
362 assert!(!is_valid_sql_expression("sys.tables"));
363 assert!(!is_valid_sql_expression("SELECT FROM information_schema"));
364 }
365
366 #[test]
367 fn test_sqli_hex_encoding() {
368 assert!(!is_valid_sql_expression("0x27")); assert!(!is_valid_sql_expression("0x4F5220313D31")); assert!(!is_valid_sql_expression("\\x27"));
372 assert!(!is_valid_sql_expression("CHAR(0x27)"));
373 }
374
375 #[test]
376 fn test_sqli_keyword_boundary_detection() {
377 assert!(is_valid_sql_expression("order_id")); assert!(is_valid_sql_expression("reorder_count")); assert!(is_valid_sql_expression("group_name")); assert!(is_valid_sql_expression("ungroup")); assert!(is_valid_sql_expression("from_date")); assert!(is_valid_sql_expression("wherefrom")); assert!(is_valid_sql_expression("selected_items")); assert!(is_valid_sql_expression("preselect")); assert!(is_valid_sql_expression("delete_flag")); assert!(is_valid_sql_expression("undelete")); assert!(is_valid_sql_expression("update_time")); assert!(is_valid_sql_expression("last_updated")); assert!(!is_valid_sql_expression("ORDER BY name"));
393 assert!(!is_valid_sql_expression("GROUP BY id"));
394 assert!(!is_valid_sql_expression("FROM users"));
395 assert!(!is_valid_sql_expression("WHERE id=1"));
396 assert!(!is_valid_sql_expression("SELECT *"));
397 assert!(!is_valid_sql_expression("DELETE FROM"));
398 assert!(!is_valid_sql_expression("UPDATE SET"));
399 }
400
401 #[test]
402 fn test_sqli_case_variations() {
403 assert!(!is_valid_sql_expression("SELECT"));
405 assert!(!is_valid_sql_expression("select"));
406 assert!(!is_valid_sql_expression("SeLeCt"));
407 assert!(!is_valid_sql_expression("sElEcT"));
408
409 assert!(!is_valid_sql_expression("UNION"));
410 assert!(!is_valid_sql_expression("union"));
411 assert!(!is_valid_sql_expression("UnIoN"));
412
413 assert!(!is_valid_sql_expression("DROP"));
414 assert!(!is_valid_sql_expression("drop"));
415 assert!(!is_valid_sql_expression("DrOp"));
416 }
417
418 #[test]
419 fn test_sqli_whitespace_variations() {
420 assert!(!is_valid_sql_expression("SELECT\t*"));
422 assert!(!is_valid_sql_expression("SELECT\n*"));
423 assert!(!is_valid_sql_expression(" SELECT "));
424 assert!(!is_valid_sql_expression("DROP\t\tTABLE"));
425 }
426
427 #[test]
428 fn test_sqli_expression_length_limit() {
429 let long_expr = "a".repeat(1001);
431 assert!(!is_valid_sql_expression(&long_expr));
432
433 let at_limit = "a".repeat(1000);
435 assert!(is_valid_sql_expression(&at_limit));
436 }
437
438 #[test]
439 fn test_valid_safe_expressions() {
440 assert!(is_valid_sql_expression("first_name || ' ' || last_name"));
442 assert!(is_valid_sql_expression("price * quantity"));
443 assert!(is_valid_sql_expression("price * 1.15")); assert!(is_valid_sql_expression(
445 "COALESCE(nickname, first_name, 'Anonymous')"
446 ));
447 assert!(is_valid_sql_expression("UPPER(TRIM(name))"));
448 assert!(is_valid_sql_expression("LENGTH(description)"));
449 assert!(is_valid_sql_expression("ABS(balance)"));
450 assert!(is_valid_sql_expression("ROUND(price, 2)"));
451 assert!(is_valid_sql_expression("LOWER(email)"));
452 assert!(is_valid_sql_expression("created_at + INTERVAL '1 day'"));
453 assert!(is_valid_sql_expression("age >= 18"));
454 assert!(is_valid_sql_expression("status = 'active'"));
455 assert!(is_valid_sql_expression("NOT is_deleted"));
456 assert!(is_valid_sql_expression("(price > 0) AND (quantity > 0)"));
457 }
458}