security_rust/injection/
ssti.rs1use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r"\{\{.*?\}\}").unwrap(),
10 Regex::new(r"\$\{.*?\}").unwrap(),
11 Regex::new(r"\{%\s*.*?\s*%\}").unwrap(),
12 Regex::new(r"<%=").unwrap(),
13 Regex::new(r"<%@").unwrap(),
14 Regex::new(r"#set\s*\(").unwrap(),
15 Regex::new(r"__mro__").unwrap(),
16 Regex::new(r"__subclasses__").unwrap(),
17 Regex::new(r"__globals__").unwrap(),
18 Regex::new(r"__builtins__").unwrap(),
19 Regex::new(r"__class__").unwrap(),
20 ]
21});
22
23pub struct SstiDetector;
24
25impl Detector for SstiDetector {
26 fn name(&self) -> &'static str {
27 "ssti"
28 }
29
30 fn detect(&self, input: &str) -> Option<DetectionResult> {
31 regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Critical, "Server-Side Template Injection detected", input)
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 fn det() -> SstiDetector {
40 SstiDetector
41 }
42
43 fn assert_hit(input: &str) {
44 crate::test_helpers::assert_detected(
45 &det(),
46 input,
47 AttackCategory::Injection,
48 Severity::Critical,
49 );
50 }
51
52 #[test]
53 fn name_is_ssti() {
54 assert_eq!(det().name(), "ssti");
55 }
56
57 #[test]
58 fn detects_common_payloads() {
59 for input in [
60 "{{7*7}}",
61 "{{ ''.__class__.__mro__[1].__subclasses__() }}",
62 "${7*7}",
63 "{% include '/etc/passwd' %}",
64 "<%= params[:x] %>",
65 r#"<%@ page import="java.util.*" %>"#,
66 "#set($x = 5)",
67 "{{config.__init__.__globals__}}",
68 ] {
69 assert_hit(input);
70 }
71 }
72
73 #[test]
74 fn benign_inputs_not_detected() {
75 for input in [
76 "Hello, this is a normal text input. Nothing suspicious here.",
77 "The total is $5.00 plus tax",
78 "Please enter your name below",
79 "The class of 2026 graduates in May",
80 "100% of users agree with this",
81 ] {
82 assert!(det().detect(input).is_none(), "false positive: {input}");
83 }
84 }
85
86 #[test]
87 fn edge_cases() {
88 assert!(det().detect("").is_none());
89 assert!(det().detect(" \t\n ").is_none());
90 assert!(det().detect("你好世界 こんにちは").is_none());
91 assert!(det().detect("{7*7}").is_none());
93 assert!(det().detect("{{7*7").is_none());
94 assert!(det().detect("__CLASS__").is_none());
95 assert!(det().detect("{$x=7}").is_none());
96 }
97
98 #[test]
99 fn obfuscated_variants_detected() {
100 for input in [
101 "{{ ''.__class__.__MRO__[1] }}",
102 "{{ self.__dict__ }}",
103 "{{request.application.__globals__}}",
104 ] {
105 assert_hit(input);
106 }
107 }
108}