1use regex::RegexSet;
5use tracing::warn;
6use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
7
8use crate::{all_matches, body_str_values, inspectable_header_values, Rule};
9
10pub static SQLI_RULES: &[Rule] = &[
13 Rule {
14 id: "sqli-union-select",
15 pattern: r"(?i)\bunion\s+(?:all\s+)?select\b",
16 severity: Severity::Critical,
17 paranoia: 1,
18 },
19 Rule {
20 id: "sqli-tautology-or",
21 pattern: r#"(?i)\bor\s+(?:\d+|['"`]?\w['"`]?)\s*=\s*(?:\d+|['"`]?\w['"`]?)"#,
27 severity: Severity::Critical,
28 paranoia: 1,
29 },
30 Rule {
31 id: "sqli-stacked-query",
32 pattern: r"(?i);\s*(?:select|insert|update|delete|drop|truncate|exec(?:ute)?|call)\b",
34 severity: Severity::Critical,
35 paranoia: 1,
36 },
37 Rule {
38 id: "sqli-time-based",
39 pattern: r"(?i)\b(?:sleep|pg_sleep|waitfor\s+delay|benchmark)\s*\(",
40 severity: Severity::Critical,
41 paranoia: 1,
42 },
43 Rule {
44 id: "sqli-mysql-versioned-comment",
45 pattern: r"(?i)/\*!\d*\s*(?:union|select|insert|update|delete|drop|alter|or|and|where|from|having|exec|cast|concat|sleep)",
51 severity: Severity::Critical,
52 paranoia: 1,
53 },
54 Rule {
55 id: "sqli-information-schema",
56 pattern: r"(?i)\binformation_schema\b",
61 severity: Severity::Critical,
62 paranoia: 1,
63 },
64 Rule {
65 id: "sqli-json-function",
66 pattern: r"(?i)\bjson_(?:extract|depth|keys|search|contains|contains_path|value|arrayagg|objectagg|object|array|quote|unquote|type|valid|length|merge\w*|set|insert|replace|remove|overlaps|table|pretty|storage_\w+)\s*\(",
70 severity: Severity::Critical,
71 paranoia: 1,
72 },
73 Rule {
74 id: "sqli-tautology-and",
75 pattern: r#"(?i)\band\s+(?:\d+|['"`]?\w['"`]?)\s*=\s*(?:\d+|['"`]?\w['"`]?)"#,
77 severity: Severity::Warning,
78 paranoia: 2,
79 },
80 Rule {
81 id: "sqli-quote-comment",
82 pattern: r#"(?i)['"`]\s*(?:--|#)"#,
84 severity: Severity::Warning,
85 paranoia: 2,
86 },
87 Rule {
88 id: "sqli-cast-convert",
89 pattern: r"(?i)\b(?:cast|convert)\s*\(",
91 severity: Severity::Notice,
92 paranoia: 3,
93 },
94 Rule {
95 id: "sqli-hex-literal",
96 pattern: r"(?i)\b0x[0-9a-f]{6,}\b",
99 severity: Severity::Notice,
100 paranoia: 3,
101 },
102 Rule {
103 id: "sqli-mssql-dangerous-proc",
104 pattern: r"(?i)(?:[.;(=]\s*|\bexec(?:ute)?\s+(?:[\w$]+\.)*)(?:xp_cmdshell|xp_dirtree|xp_fileexist|xp_reg(?:read|write|deletekey|deletevalue|enumvalues)|sp_oacreate|sp_oamethod|sp_makewebtask|xp_servicecontrol|xp_availablemedia)\b",
112 severity: Severity::Critical,
113 paranoia: 1,
114 },
115];
116
117#[derive(Default)]
120pub struct SqliModule {
121 rule_set: Option<RegexSet>,
122 active_rules: Vec<&'static Rule>,
124}
125
126impl SqliModule {
127 pub fn new() -> Self {
128 Self::default()
129 }
130}
131
132impl WafModule for SqliModule {
133 fn id(&self) -> &str {
134 "sqli"
135 }
136
137 fn phase(&self) -> Phase {
138 Phase::Body
139 }
140
141 fn init(&mut self, cfg: &Config) {
142 let pl = cfg.waf.paranoia_level;
143 self.active_rules = SQLI_RULES.iter().filter(|r| r.paranoia <= pl).collect();
144 self.rule_set = Some(
145 RegexSet::new(self.active_rules.iter().map(|r| r.pattern))
146 .expect("SQLi rule compilation failed — check patterns at startup"),
147 );
148 }
149
150 fn inspect(&self, ctx: &RequestContext) -> Decision {
151 let Some(rule_set) = &self.rule_set else {
152 return Decision::Allow;
153 };
154
155 let query = ctx.normalized.query_params.iter().map(|(_, v)| v.as_str());
156 let cookies = ctx.normalized.cookies.iter().map(|(_, v)| v.as_str());
157 let body_vals = body_str_values(&ctx.normalized.body);
158 let body = body_vals.iter().map(String::as_str);
159 let derived = ctx.normalized.derived_decoded.iter().map(String::as_str);
160
161 let path = std::iter::once(ctx.normalized.path.as_str());
165 let headers = inspectable_header_values(ctx);
167 let matched = all_matches(rule_set, path.chain(query).chain(cookies).chain(body).chain(derived).chain(headers));
168 if matched.is_empty() {
169 return Decision::Allow;
170 }
171
172 let items: Vec<ScoreItem> = matched
173 .iter()
174 .map(|&idx| {
175 let rule = self.active_rules[idx];
176 warn!(
177 request_id = %ctx.request_id,
178 rule_id = %rule.id,
179 severity = ?rule.severity,
180 "sqli detection"
181 );
182 ScoreItem {
183 rule_id: rule.id.to_string(),
184 severity: rule.severity,
185 }
186 })
187 .collect();
188
189 Decision::Scores(items)
190 }
191}