use std::sync::OnceLock;
use regex::RegexSet;
static SSTI_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static SSTI_DESCRIPTIONS: &[&str] = &[
"SSTI: Jinja2/Twig expression ({{ }})",
"SSTI: ERB expression (<%= %>)",
"SSTI: arithmetic probe ({{7*7}}, ${7*7})",
"SSTI: class/module access (__class__, __mro__)",
"SSTI: Jinja2 import/popen pattern",
"SSTI: Freemarker built-in (.exec, .new)",
];
fn patterns() -> &'static RegexSet {
SSTI_PATTERNS.get_or_init(|| {
RegexSet::new([
r"\{\{[^}]*(config|request|self|class|import|popen|subprocess|os\.|system|eval|exec|globals|builtins|lipsum)",
r"<%[=-]?\s*(system|exec|eval|IO\.|File\.|require|`)",
r"(\{\{|\$\{|#\{)\d+\s*\*\s*\d+\s*(\}\}|\})",
r"(?i)(__class__|__mro__|__subclasses__|__builtins__|__globals__|__import__)",
r"(?i)\{\{[^}]*(import\s*\(|popen|subprocess|os\.system)",
r"(?i)\$\{[^}]*\.(exec|new|getClass|forName)\s*\(",
])
.expect("SSTI regex patterns must compile")
})
}
pub fn check_ssti(input: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(input).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
SSTI_DESCRIPTIONS
.get(idx)
.unwrap_or(&"template injection")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_jinja2_config() {
assert!(check_ssti("{{config}}").is_some());
assert!(check_ssti("{{request.application.__globals__}}").is_some());
}
#[test]
fn detects_arithmetic_probe() {
assert!(check_ssti("{{7*7}}").is_some());
assert!(check_ssti("${7*7}").is_some());
assert!(check_ssti("#{7*7}").is_some());
}
#[test]
fn detects_dunder_access() {
assert!(check_ssti("''.__class__.__mro__").is_some());
assert!(check_ssti("__builtins__.__import__('os')").is_some());
}
#[test]
fn detects_erb() {
assert!(check_ssti("<%= system('id') %>").is_some());
assert!(check_ssti("<%= `whoami` %>").is_some());
}
#[test]
fn detects_freemarker() {
assert!(check_ssti("${\"freemarker\".exec('id')}").is_some());
}
#[test]
fn allows_normal_text() {
assert!(check_ssti("Hello world").is_none());
assert!(check_ssti("/api/users/123").is_none());
}
#[test]
fn allows_normal_json() {
assert!(check_ssti(r#"{"key": "value"}"#).is_none());
}
}