Skip to main content

security_rust/injection/
ssti.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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        // near misses: delimiters incomplete, or magic names uppercase (case-sensitive)
92        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}