Skip to main content

wafrift_oracle/
lib.rs

1//! Payload oracles — semantic validation across injection types.
2//!
3//! The oracle system ensures that evasion transforms preserve exploit
4//! semantics. Each oracle understands the structural invariants of a
5//! specific injection type and rejects transforms that would render
6//! the payload inert.
7//!
8//! # Architecture
9//!
10//! ```text
11//! PayloadOracle (trait)
12//! ├── SqlOracle       — SQL AST parsing via sqlparser
13//! ├── XssOracle       — HTML tag/event/exec structure validation
14//! ├── SstiOracle      — Template delimiter and expression validation
15//! ├── CmdiOracle      — Shell separator + command validation
16//! ├── PathOracle      — Directory traversal sequence validation
17//! ├── LdapOracle      — LDAP filter syntax validation
18//! └── SsrfOracle      — URL structure and host validation
19//! ```
20//!
21//! # Usage
22//!
23//! ```rust
24//! use wafrift_oracle::traits::PayloadOracle;
25//! use wafrift_oracle::xss::XssOracle;
26//!
27//! let oracle = XssOracle;
28//! assert!(oracle.is_semantically_valid(
29//!     "<script>alert(1)</script>",
30//!     "<ScRiPt>alert(1)</sCrIpT>",
31//! ));
32//! ```
33//!
34//! Pick the right oracle dynamically from the classified payload
35//! type — every grammar in `wafrift-grammar` has a matching oracle:
36//!
37//! ```
38//! use wafrift_grammar::PayloadType;
39//! use wafrift_oracle::oracle_for;
40//!
41//! let oracle = oracle_for(PayloadType::Sql).unwrap();
42//! assert_eq!(oracle.name(), "SQL");
43//! assert!(oracle.is_semantically_valid("1 OR 1=1 --", "1 OR 1=1 --"));
44//! // Mutilated payload that no longer parses as SQL: rejected.
45//! assert!(!oracle.is_semantically_valid("1 OR 1=1 --", "1 O R 1=1 --"));
46//! ```
47//!
48//! Reject SSRF mutations that lose the loopback target (a
49//! transformation engine can call this before emitting a variant):
50//!
51//! ```
52//! use wafrift_oracle::ssrf::SsrfOracle;
53//! use wafrift_oracle::traits::PayloadOracle;
54//!
55//! let oracle = SsrfOracle;
56//! // Same target, different on-the-wire encoding — kept.
57//! assert!(oracle.is_semantically_valid("http://127.0.0.1/", "http://127.1/"));
58//! // Pivot to a public host — semantics lost, rejected.
59//! assert!(!oracle.is_semantically_valid("http://127.0.0.1/", "http://example.com/"));
60//! ```
61
62/// Per-target calibration session.
63pub mod calibration;
64/// Command injection oracle.
65pub mod cmdi;
66/// LDAP injection oracle.
67pub mod ldap;
68/// Path traversal oracle.
69pub mod path;
70/// WAF response oracle.
71pub mod response_oracle;
72/// Body-marker signal extractor.
73pub mod signal_body_marker;
74mod ascii_scan;
75
76/// Connection-behavior signal extractor.
77pub mod signal_connection;
78/// H2 GOAWAY signal extractor.
79pub mod signal_h2_goaway;
80/// Response header signal extractor.
81pub mod signal_headers;
82/// Response-time signal extractor.
83pub mod signal_response_time;
84/// Status-code signal extractor.
85pub mod signal_status_code;
86/// SQL AST oracle.
87pub mod sql;
88/// SSRF (Server-Side Request Forgery) oracle.
89pub mod ssrf;
90/// SSTI (Server-Side Template Injection) oracle.
91pub mod ssti;
92/// Oracle trait definition.
93pub mod traits;
94/// XSS (Cross-Site Scripting) oracle.
95pub mod xss;
96
97use traits::PayloadOracle;
98use wafrift_grammar::grammar::PayloadType;
99
100/// SQL oracle adapter that implements the `PayloadOracle` trait.
101///
102/// Wraps the existing `sql::is_valid_expression_injection` function
103/// behind the unified trait interface.
104pub struct SqlOracle {
105    /// SQL dialect to validate against.
106    pub dialect: sql::DatabaseDialect,
107}
108
109impl SqlOracle {
110    /// Create an oracle for the given dialect.
111    #[must_use]
112    pub fn new(dialect: sql::DatabaseDialect) -> Self {
113        Self { dialect }
114    }
115
116    /// Create an oracle using the generic ANSI SQL dialect.
117    #[must_use]
118    pub fn generic() -> Self {
119        Self::new(sql::DatabaseDialect::Generic)
120    }
121}
122
123impl PayloadOracle for SqlOracle {
124    fn is_semantically_valid(&self, _original: &str, transformed: &str) -> bool {
125        sql::is_valid_expression_injection(transformed, self.dialect)
126    }
127
128    fn name(&self) -> &'static str {
129        "SQL"
130    }
131}
132
133/// Select the appropriate oracle for a given payload type.
134///
135/// Returns a boxed trait object that can validate payload transforms
136/// for the detected injection type.
137///
138/// # Returns
139///
140/// `None` for `PayloadType::Unknown` — no oracle can validate an
141/// unknown payload type without risk of false positives.
142#[must_use]
143pub fn oracle_for(payload_type: PayloadType) -> Option<Box<dyn PayloadOracle>> {
144    match payload_type {
145        PayloadType::Sql => Some(Box::new(SqlOracle::generic())),
146        PayloadType::Xss => Some(Box::new(xss::XssOracle)),
147        PayloadType::TemplateInjection => Some(Box::new(ssti::SstiOracle)),
148        PayloadType::CommandInjection => Some(Box::new(cmdi::CmdiOracle)),
149        PayloadType::PathTraversal => Some(Box::new(path::PathOracle)),
150        PayloadType::Ldap => Some(Box::new(ldap::LdapOracle)),
151        PayloadType::Ssrf => Some(Box::new(ssrf::SsrfOracle)),
152        // Future-proof: new payload types get oracles when they're built.
153        // Until then, returning None means "don't validate" — safe default.
154        _ => None,
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn sql_oracle_adapter_valid() {
164        let oracle = SqlOracle::generic();
165        assert!(oracle.is_semantically_valid("1 OR 1=1 --", "1 OR 1=1 --",));
166    }
167
168    #[test]
169    fn sql_oracle_adapter_invalid() {
170        let oracle = SqlOracle::generic();
171        assert!(!oracle.is_semantically_valid("1 OR 1=1 --", "1 O R 1=1 --",));
172    }
173
174    #[test]
175    fn oracle_for_sql() {
176        let oracle = oracle_for(PayloadType::Sql);
177        assert!(oracle.is_some());
178        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("SQL"));
179    }
180
181    #[test]
182    fn oracle_for_xss() {
183        let oracle = oracle_for(PayloadType::Xss);
184        assert!(oracle.is_some());
185        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("XSS"));
186    }
187
188    #[test]
189    fn oracle_for_ssti() {
190        let oracle = oracle_for(PayloadType::TemplateInjection);
191        assert!(oracle.is_some());
192        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("SSTI"));
193    }
194
195    #[test]
196    fn oracle_for_cmdi() {
197        let oracle = oracle_for(PayloadType::CommandInjection);
198        assert!(oracle.is_some());
199        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("CMDI"));
200    }
201
202    #[test]
203    fn oracle_for_path() {
204        let oracle = oracle_for(PayloadType::PathTraversal);
205        assert!(oracle.is_some());
206        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("PathTraversal"));
207    }
208
209    #[test]
210    fn oracle_for_unknown_is_none() {
211        let oracle = oracle_for(PayloadType::Unknown);
212        assert!(oracle.is_none());
213    }
214
215    #[test]
216    fn oracle_for_ldap() {
217        let oracle = oracle_for(PayloadType::Ldap);
218        assert!(oracle.is_some());
219        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("LDAP"));
220    }
221
222    #[test]
223    fn oracle_for_ssrf() {
224        let oracle = oracle_for(PayloadType::Ssrf);
225        assert!(oracle.is_some());
226        assert_eq!(oracle.as_ref().map(|o| o.name()), Some("SSRF"));
227    }
228
229    #[test]
230    fn ldap_oracle_validates_filter_structure() {
231        let oracle = ldap::LdapOracle;
232        // Valid LDAP filter
233        assert!(oracle.is_semantically_valid("(uid=admin)", "(uid=admin)"));
234        // Boolean operator
235        assert!(
236            oracle.is_semantically_valid("(|(uid=admin)(uid=root))", "(|(uid=admin)(uid=root))",)
237        );
238    }
239
240    #[test]
241    fn ldap_oracle_rejects_invalid() {
242        let oracle = ldap::LdapOracle;
243        // No parentheses
244        assert!(!oracle.is_semantically_valid("(uid=admin)", "uid=admin"));
245        // Empty
246        assert!(!oracle.is_semantically_valid("(uid=admin)", ""));
247    }
248
249    #[test]
250    fn ssrf_oracle_validates_url_structure() {
251        let oracle = ssrf::SsrfOracle;
252        // Valid SSRF URL
253        assert!(oracle.is_semantically_valid("http://127.0.0.1/admin", "http://127.0.0.1/admin",));
254        // AWS metadata
255        assert!(
256            oracle.is_semantically_valid("http://169.254.169.254/", "http://169.254.169.254/",)
257        );
258    }
259
260    #[test]
261    fn ssrf_oracle_rejects_invalid() {
262        let oracle = ssrf::SsrfOracle;
263        // No scheme
264        assert!(!oracle.is_semantically_valid("http://127.0.0.1/", "127.0.0.1"));
265        // Public URL
266        assert!(!oracle.is_semantically_valid("http://127.0.0.1/", "http://example.com/"));
267    }
268}
269
270pub mod oob;