1#[derive(Debug)]
37pub enum GuardError {
38 FullTableUpdate {
40 table: String,
42 },
43 FullTableDelete {
45 table: String,
47 },
48 ParseError(String),
50}
51
52impl std::fmt::Display for GuardError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 GuardError::FullTableUpdate { table } => write!(
56 f,
57 "Blocked full-table UPDATE on `{}` (no WHERE clause). Add WHERE clause or use GuardPolicy::Permissive.",
58 table
59 ),
60 GuardError::FullTableDelete { table } => write!(
61 f,
62 "Blocked full-table DELETE on `{}` (no WHERE clause). Add WHERE clause or use GuardPolicy::Permissive.",
63 table
64 ),
65 GuardError::ParseError(msg) => write!(f, "SQL parse error in guard: {}", msg),
66 }
67 }
68}
69
70impl std::error::Error for GuardError {}
71
72pub type GuardResult<T> = Result<T, GuardError>;
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum GuardPolicy {
82 #[default]
84 Strict,
85 Permissive,
87 Disabled,
89}
90
91#[derive(Debug, Clone, Copy)]
116pub struct SafeSqlGuard {
117 pub policy: GuardPolicy,
119}
120
121impl SafeSqlGuard {
122 pub fn new(policy: GuardPolicy) -> Self {
124 Self { policy }
125 }
126
127 pub fn strict() -> Self {
129 Self::new(GuardPolicy::Strict)
130 }
131
132 pub fn permissive() -> Self {
134 Self::new(GuardPolicy::Permissive)
135 }
136
137 pub fn disabled() -> Self {
139 Self::new(GuardPolicy::Disabled)
140 }
141
142 pub fn check(&self, sql: &str) -> GuardResult<()> {
144 match self.policy {
145 GuardPolicy::Disabled => Ok(()),
146 GuardPolicy::Permissive => {
147 Ok(())
150 }
151 GuardPolicy::Strict => check_sql_strict(sql),
152 }
153 }
154
155 pub fn check_update(&self, sql: &str) -> GuardResult<()> {
157 self.check(sql)
158 }
159
160 pub fn check_delete(&self, sql: &str) -> GuardResult<()> {
162 self.check(sql)
163 }
164}
165
166impl Default for SafeSqlGuard {
167 fn default() -> Self {
168 Self::strict()
169 }
170}
171
172fn check_sql_strict(sql: &str) -> GuardResult<()> {
188 let normalized = normalize_sql(sql);
189 let upper = normalized.to_uppercase();
190
191 if upper.starts_with("UPDATE") {
193 let table = extract_update_table(&normalized, &upper);
194 if !has_where_after_keyword(&upper, "SET") {
196 return Err(GuardError::FullTableUpdate {
197 table: table.unwrap_or_else(|| "unknown".to_string()),
198 });
199 }
200 }
201
202 if upper.starts_with("DELETE") {
204 let table = extract_delete_table(&normalized, &upper);
205 if !has_where_after_keyword(&upper, "FROM") {
207 return Err(GuardError::FullTableDelete {
208 table: table.unwrap_or_else(|| "unknown".to_string()),
209 });
210 }
211 }
212
213 Ok(())
214}
215
216fn normalize_sql(sql: &str) -> String {
218 let without_line_comments: String = sql
220 .lines()
221 .map(|line| {
222 if let Some(idx) = line.find("--") {
223 &line[..idx]
224 } else {
225 line
226 }
227 })
228 .collect::<Vec<&str>>()
229 .join(" ");
230
231 let mut without_block_comments = String::with_capacity(without_line_comments.len());
233 let mut in_block_comment = false;
234 let mut chars = without_line_comments.chars().peekable();
235 while let Some(c) = chars.next() {
236 if c == '/' && chars.peek() == Some(&'*') {
237 in_block_comment = true;
238 chars.next(); continue;
240 }
241 if in_block_comment && c == '*' && chars.peek() == Some(&'/') {
242 in_block_comment = false;
243 chars.next(); continue;
245 }
246 if !in_block_comment {
247 without_block_comments.push(c);
248 }
249 }
250
251 let mut result = String::with_capacity(without_block_comments.len());
253 let mut prev_whitespace = false;
254 for c in without_block_comments.chars() {
255 if c.is_whitespace() {
256 if !prev_whitespace {
257 result.push(' ');
258 prev_whitespace = true;
259 }
260 } else {
261 result.push(c);
262 prev_whitespace = false;
263 }
264 }
265 result.trim().to_string()
266}
267
268fn has_where_after_keyword(upper_sql: &str, keyword: &str) -> bool {
281 let kw_pos = match find_keyword_independent(upper_sql, keyword) {
283 Some(idx) => idx,
284 None => return false,
285 };
286
287 let after_kw = &upper_sql[kw_pos + keyword.len()..];
289 let where_idx = match find_where_at_depth_zero(after_kw) {
290 Some(idx) => idx,
291 None => return false,
292 };
293
294 let after_where = after_kw[where_idx + 5..].trim();
296 !after_where.is_empty()
297}
298
299fn find_where_at_depth_zero(sql: &str) -> Option<usize> {
307 let bytes = sql.as_bytes();
308 let mut depth: i32 = 0;
309 let mut i = 0;
310
311 while i + 5 <= bytes.len() {
312 let c = bytes[i];
313
314 if c == b'(' {
316 depth += 1;
317 i += 1;
318 continue;
319 }
320 if c == b')' {
321 depth -= 1;
322 if depth < 0 {
323 depth = 0;
324 }
325 i += 1;
326 continue;
327 }
328
329 if depth == 0 && &bytes[i..i + 5] == b"WHERE" {
331 let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
333 let next_idx = i + 5;
335 let next_ok = next_idx >= bytes.len()
336 || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
337 if prev_ok && next_ok {
338 return Some(i);
339 }
340 }
341
342 i += 1;
343 }
344
345 None
346}
347
348fn find_keyword_independent(sql: &str, keyword: &str) -> Option<usize> {
353 let kw_len = keyword.len();
354 if kw_len == 0 || sql.len() < kw_len {
355 return None;
356 }
357
358 let bytes = sql.as_bytes();
359 let kw_bytes = keyword.as_bytes();
360
361 let mut i = 0;
362 while i + kw_len <= bytes.len() {
363 if &bytes[i..i + kw_len] == kw_bytes {
364 let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
366 let next_idx = i + kw_len;
368 let next_ok = next_idx >= bytes.len()
369 || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
370 if prev_ok && next_ok {
371 return Some(i);
372 }
373 }
374 i += 1;
375 }
376 None
377}
378
379fn extract_update_table(sql: &str, upper: &str) -> Option<String> {
387 let update_end = upper.find("UPDATE").map(|i| i + 6)?;
389 let set_idx = upper.find(" SET ")?;
390
391 let between = sql[update_end..set_idx].trim();
392
393 let table = if (between.starts_with('`') && between.ends_with('`'))
395 || (between.starts_with('"') && between.ends_with('"'))
396 {
397 between[1..between.len() - 1].to_string()
398 } else {
399 between.to_string()
400 };
401
402 let table = table.rsplit('.').next().unwrap_or(&table).to_string();
404
405 if table.is_empty() {
406 None
407 } else {
408 Some(table)
409 }
410}
411
412fn extract_delete_table(sql: &str, upper: &str) -> Option<String> {
420 if let Some(from_idx) = upper.find("DELETE FROM") {
422 let after_from = sql[from_idx + 11..].trim_start();
423 let end = after_from
425 .find(|c: char| c.is_whitespace())
426 .unwrap_or(after_from.len());
427 let raw = after_from[..end].trim();
428
429 return parse_table_name(raw);
430 }
431
432 if upper.starts_with("DELETE ") {
434 let after_delete = sql[7..].trim_start();
435 let end = after_delete
436 .find(|c: char| c.is_whitespace())
437 .unwrap_or(after_delete.len());
438 let raw = after_delete[..end].trim();
439 return parse_table_name(raw);
440 }
441
442 None
443}
444
445fn parse_table_name(raw: &str) -> Option<String> {
447 let table = if (raw.starts_with('`') && raw.ends_with('`') && raw.len() >= 2)
448 || (raw.starts_with('"') && raw.ends_with('"') && raw.len() >= 2)
449 {
450 raw[1..raw.len() - 1].to_string()
451 } else {
452 raw.to_string()
453 };
454
455 let table = table.rsplit('.').next().unwrap_or(&table).to_string();
456
457 if table.is_empty() {
458 None
459 } else {
460 Some(table)
461 }
462}
463
464#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
475 fn test_strict_blocks_update_without_where() {
476 let guard = SafeSqlGuard::strict();
477 let result = guard.check("UPDATE users SET name = 'a'");
478 assert!(matches!(
479 result,
480 Err(GuardError::FullTableUpdate { table }) if table == "users"
481 ));
482 }
483
484 #[test]
485 fn test_strict_blocks_update_without_where_multiple_columns() {
486 let guard = SafeSqlGuard::strict();
487 let result = guard.check("UPDATE users SET name = 'a', age = 30, status = 'active'");
488 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
489 }
490
491 #[test]
492 fn test_strict_allows_update_with_where() {
493 let guard = SafeSqlGuard::strict();
494 let result = guard.check("UPDATE users SET name = 'a' WHERE id = 1");
495 assert!(result.is_ok());
496 }
497
498 #[test]
499 fn test_strict_allows_update_with_where_in() {
500 let guard = SafeSqlGuard::strict();
501 let result = guard.check("UPDATE users SET name = 'a' WHERE id IN (1, 2, 3)");
502 assert!(result.is_ok());
503 }
504
505 #[test]
506 fn test_strict_blocks_update_with_quoted_table() {
507 let guard = SafeSqlGuard::strict();
508 let result = guard.check("UPDATE `users` SET name = 'a'");
509 assert!(matches!(
510 result,
511 Err(GuardError::FullTableUpdate { table }) if table == "users"
512 ));
513 }
514
515 #[test]
516 fn test_strict_blocks_update_with_double_quoted_table() {
517 let guard = SafeSqlGuard::strict();
518 let result = guard.check("UPDATE \"users\" SET name = 'a'");
519 assert!(matches!(
520 result,
521 Err(GuardError::FullTableUpdate { table }) if table == "users"
522 ));
523 }
524
525 #[test]
526 fn test_strict_blocks_update_with_schema_qualified_table() {
527 let guard = SafeSqlGuard::strict();
528 let result = guard.check("UPDATE public.users SET name = 'a'");
529 assert!(matches!(
530 result,
531 Err(GuardError::FullTableUpdate { table }) if table == "users"
532 ));
533 }
534
535 #[test]
538 fn test_strict_blocks_delete_without_where() {
539 let guard = SafeSqlGuard::strict();
540 let result = guard.check("DELETE FROM users");
541 assert!(matches!(
542 result,
543 Err(GuardError::FullTableDelete { table }) if table == "users"
544 ));
545 }
546
547 #[test]
548 fn test_strict_allows_delete_with_where() {
549 let guard = SafeSqlGuard::strict();
550 let result = guard.check("DELETE FROM users WHERE id = 1");
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn test_strict_blocks_delete_with_quoted_table() {
556 let guard = SafeSqlGuard::strict();
557 let result = guard.check("DELETE FROM `users`");
558 assert!(matches!(
559 result,
560 Err(GuardError::FullTableDelete { table }) if table == "users"
561 ));
562 }
563
564 #[test]
565 fn test_strict_blocks_delete_mysql_extension() {
566 let guard = SafeSqlGuard::strict();
569 let result = guard.check("DELETE users");
570 assert!(matches!(
571 result,
572 Err(GuardError::FullTableDelete { table }) if table == "users"
573 ));
574 }
575
576 #[test]
579 fn test_permissive_allows_update_without_where() {
580 let guard = SafeSqlGuard::permissive();
581 let result = guard.check("UPDATE users SET name = 'a'");
582 assert!(result.is_ok());
583 }
584
585 #[test]
586 fn test_permissive_allows_delete_without_where() {
587 let guard = SafeSqlGuard::permissive();
588 let result = guard.check("DELETE FROM users");
589 assert!(result.is_ok());
590 }
591
592 #[test]
595 fn test_disabled_allows_everything() {
596 let guard = SafeSqlGuard::disabled();
597 assert!(guard.check("UPDATE users SET name = 'a'").is_ok());
598 assert!(guard.check("DELETE FROM users").is_ok());
599 }
600
601 #[test]
604 fn test_strict_allows_select_without_where() {
605 let guard = SafeSqlGuard::strict();
606 let result = guard.check("SELECT * FROM users");
607 assert!(result.is_ok());
608 }
609
610 #[test]
611 fn test_strict_allows_insert_without_where() {
612 let guard = SafeSqlGuard::strict();
613 let result = guard.check("INSERT INTO users (name) VALUES ('a')");
614 assert!(result.is_ok());
615 }
616
617 #[test]
618 fn test_strict_allows_create_table() {
619 let guard = SafeSqlGuard::strict();
620 let result = guard.check("CREATE TABLE users (id INT)");
621 assert!(result.is_ok());
622 }
623
624 #[test]
627 fn test_strict_blocks_multiline_update_without_where() {
628 let guard = SafeSqlGuard::strict();
629 let sql = "UPDATE users\nSET name = 'a',\n age = 30";
630 let result = guard.check(sql);
631 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
632 }
633
634 #[test]
635 fn test_strict_allows_multiline_update_with_where() {
636 let guard = SafeSqlGuard::strict();
637 let sql = "UPDATE users\nSET name = 'a'\nWHERE id = 1";
638 let result = guard.check(sql);
639 assert!(result.is_ok());
640 }
641
642 #[test]
643 fn test_strict_blocks_update_with_line_comment_only() {
644 let guard = SafeSqlGuard::strict();
645 let sql = "UPDATE users SET name = 'a' -- WHERE id = 1";
646 let result = guard.check(sql);
647 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
649 }
650
651 #[test]
652 fn test_strict_blocks_update_with_block_comment_only() {
653 let guard = SafeSqlGuard::strict();
654 let sql = "UPDATE users SET name = 'a' /* WHERE id = 1 */";
655 let result = guard.check(sql);
656 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
657 }
658
659 #[test]
660 fn test_strict_allows_update_with_real_where_and_comment() {
661 let guard = SafeSqlGuard::strict();
662 let sql = "UPDATE users SET name = 'a' /* update name */ WHERE id = 1";
663 let result = guard.check(sql);
664 assert!(result.is_ok());
665 }
666
667 #[test]
670 fn test_strict_does_not_treat_nowhere_as_where() {
671 let guard = SafeSqlGuard::strict();
673 let sql = "UPDATE my_table SET somewhere = 'x'";
675 let result = guard.check(sql);
676 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
677 }
678
679 #[test]
682 fn test_check_update_method() {
683 let guard = SafeSqlGuard::strict();
684 assert!(guard.check_update("UPDATE users SET name = 'a'").is_err());
685 assert!(guard
686 .check_update("UPDATE users SET name = 'a' WHERE id = 1")
687 .is_ok());
688 }
689
690 #[test]
691 fn test_check_delete_method() {
692 let guard = SafeSqlGuard::strict();
693 assert!(guard.check_delete("DELETE FROM users").is_err());
694 assert!(guard.check_delete("DELETE FROM users WHERE id = 1").is_ok());
695 }
696
697 #[test]
700 fn test_default_guard_is_strict() {
701 let guard = SafeSqlGuard::default();
702 assert_eq!(guard.policy, GuardPolicy::Strict);
703 assert!(guard.check("UPDATE users SET name = 'a'").is_err());
704 }
705
706 #[test]
709 fn test_guard_error_display_full_table_update() {
710 let e = GuardError::FullTableUpdate {
711 table: "users".to_string(),
712 };
713 let s = format!("{}", e);
714 assert!(s.contains("Blocked full-table UPDATE"));
715 assert!(s.contains("users"));
716 }
717
718 #[test]
719 fn test_guard_error_display_full_table_delete() {
720 let e = GuardError::FullTableDelete {
721 table: "orders".to_string(),
722 };
723 let s = format!("{}", e);
724 assert!(s.contains("Blocked full-table DELETE"));
725 assert!(s.contains("orders"));
726 }
727
728 #[test]
731 fn test_guard_policy_default_is_strict() {
732 let p = GuardPolicy::default();
733 assert_eq!(p, GuardPolicy::Strict);
734 }
735
736 #[test]
739 fn test_strict_blocks_truncate() {
740 let guard = SafeSqlGuard::strict();
743 let result = guard.check("TRUNCATE TABLE users");
745 assert!(result.is_ok());
746 }
747
748 #[test]
749 fn test_strict_blocks_update_with_lowercase_keywords() {
750 let guard = SafeSqlGuard::strict();
751 let result = guard.check("update users set name = 'a'");
752 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
753 }
754
755 #[test]
756 fn test_strict_blocks_delete_with_lowercase_keywords() {
757 let guard = SafeSqlGuard::strict();
758 let result = guard.check("delete from users");
759 assert!(matches!(result, Err(GuardError::FullTableDelete { .. })));
760 }
761
762 #[test]
763 fn test_strict_allows_update_with_lowercase_where() {
764 let guard = SafeSqlGuard::strict();
765 let result = guard.check("update users set name = 'a' where id = 1");
766 assert!(result.is_ok());
767 }
768
769 #[test]
770 fn test_strict_blocks_update_with_empty_where() {
771 let guard = SafeSqlGuard::strict();
772 let result = guard.check("UPDATE users SET name = 'a' WHERE ");
774 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
775 }
776
777 #[test]
778 fn test_normalize_sql_collapses_whitespace() {
779 let sql = "UPDATE users\n\nSET name = 'a'";
780 let normalized = normalize_sql(sql);
781 assert_eq!(normalized, "UPDATE users SET name = 'a'");
782 }
783
784 #[test]
785 fn test_normalize_sql_removes_line_comments() {
786 let sql = "UPDATE users -- this is a comment\nSET name = 'a'";
787 let normalized = normalize_sql(sql);
788 assert!(normalized.contains("UPDATE users"));
789 assert!(!normalized.contains("this is a comment"));
790 }
791
792 #[test]
793 fn test_normalize_sql_removes_block_comments() {
794 let sql = "UPDATE users /* block comment */ SET name = 'a'";
795 let normalized = normalize_sql(sql);
796 assert!(!normalized.contains("block comment"));
797 assert!(normalized.contains("UPDATE users"));
798 assert!(normalized.contains("SET name = 'a'"));
799 }
800
801 #[test]
804 fn test_blocks_update_with_where_only_in_subquery() {
805 let guard = SafeSqlGuard::strict();
808 let sql = "UPDATE users SET name = (SELECT name FROM other WHERE id = 1)";
809 let result = guard.check(sql);
810 assert!(
811 matches!(result, Err(GuardError::FullTableUpdate { table }) if table == "users"),
812 "子查询中的 WHERE 不应被误认为外层 UPDATE 的 WHERE,应拦截"
813 );
814 }
815
816 #[test]
817 fn test_blocks_delete_with_where_only_in_subquery() {
818 let guard = SafeSqlGuard::strict();
820 let sql = "DELETE FROM users WHERE id IN (SELECT id FROM other)";
821 let result = guard.check(sql);
823 assert!(result.is_ok(), "外层 WHERE 应被识别");
824
825 let sql2 = "DELETE FROM users RETURNING (SELECT id FROM other WHERE x = 1)";
827 let result2 = guard.check(sql2);
828 assert!(
829 matches!(result2, Err(GuardError::FullTableDelete { table }) if table == "users"),
830 "子查询中的 WHERE 不应被误认为外层 DELETE 的 WHERE,应拦截"
831 );
832 }
833
834 #[test]
835 fn test_allows_update_with_real_where_and_subquery_where() {
836 let guard = SafeSqlGuard::strict();
838 let sql = "UPDATE users SET name = 'a' WHERE id IN (SELECT id FROM other WHERE active = 1)";
839 let result = guard.check(sql);
840 assert!(result.is_ok());
841 }
842
843 #[test]
844 fn test_blocks_update_with_field_named_where() {
845 let guard = SafeSqlGuard::strict();
847 let sql = "UPDATE my_table SET somewhere = 'x'";
848 let result = guard.check(sql);
849 assert!(matches!(result, Err(GuardError::FullTableUpdate { .. })));
850 }
851}