security_rust/injection/
ssi_injection.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static STRONG_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
12 Regex::new(concat!(
13 r"<!--#exec cmd=",
14 r#"|<!--#include file=["']?(?:/|\.\.)"#,
17 r"|<!--#printenv",
18 ))
19 .unwrap()
20});
21
22static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
28 Regex::new(concat!(
29 r"<!--#include file=",
30 r"|<!--#echo var=",
31 r"|<!--#fsize",
32 r"|<!--#flastmod",
33 r"|<!--#config",
34 ))
35 .unwrap()
36});
37
38pub struct SsiInjectionDetector;
39
40impl Detector for SsiInjectionDetector {
41 fn name(&self) -> &'static str {
42 "ssi_injection"
43 }
44
45 fn detect(&self, input: &str) -> Option<DetectionResult> {
46 regex_detect(
47 std::slice::from_ref(&*STRONG_PATTERNS),
48 self.name(),
49 AttackCategory::Injection,
50 Severity::High,
51 "SSI Server-Side Include injection detected",
52 input,
53 )
54 .or_else(|| {
55 regex_detect(
56 std::slice::from_ref(&*WEAK_PATTERNS),
57 self.name(),
58 AttackCategory::Injection,
59 Severity::Low,
60 "SSI directive present (weak signal)",
61 input,
62 )
63 })
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 fn det() -> SsiInjectionDetector {
72 SsiInjectionDetector
73 }
74
75 fn assert_hit_at(input: &str, severity: Severity) {
76 crate::test_helpers::assert_detected(&det(), input, AttackCategory::Injection, severity);
77 }
78
79 #[test]
80 fn name_is_ssi_injection() {
81 assert_eq!(det().name(), "ssi_injection");
82 }
83
84 #[test]
85 fn detects_common_payloads() {
86 for (input, severity) in [
87 (r#"<!--#exec cmd="cat /etc/passwd"-->"#, Severity::High),
89 (r#"<!--#include file="/etc/passwd"-->"#, Severity::High),
90 (r#"<!--#include file="../../etc/passwd"-->"#, Severity::High),
91 (r#"<!--#printenv-->"#, Severity::High),
92 (r#"<!--#echo var="DATE_LOCAL"-->"#, Severity::Low),
94 (r#"<!--#fsize file="index.html"-->"#, Severity::Low),
95 (r#"<!--#flastmod file="index.html"-->"#, Severity::Low),
96 (r#"<!--#config timefmt="%B"-->"#, Severity::Low),
97 ] {
98 assert_hit_at(input, severity);
99 }
100 }
101
102 #[test]
106 fn stock_ssi_page_is_not_high() {
107 let page = "<!--#include file=\"header.html\" -->\n\
108 <!--#echo var=\"DATE_LOCAL\" -->\n\
109 <!--#config timefmt=\"%Y\" -->\n\
110 <!--#fsize file=\"footer.html\" -->";
111 assert_hit_at(page, Severity::Low);
112 assert!(det().detect(page).is_some());
114 }
115
116 #[test]
117 fn benign_inputs_not_detected() {
118 for input in [
119 "Hello, this is a normal text input. Nothing suspicious here.",
120 "<!-- this is a plain comment -->",
121 "The page was generated at 3:00 PM",
122 "Include the file below the table",
123 "The include directive resolves the path at build time.",
124 ] {
125 assert!(det().detect(input).is_none(), "false positive: {input}");
126 }
127 }
128
129 #[test]
130 fn edge_cases() {
131 assert!(det().detect("").is_none());
132 assert!(det().detect(" \t\n ").is_none());
133 assert!(det().detect("你好世界 こんにちは").is_none());
134 assert!(det().detect("<!--#exec").is_none());
136 assert!(det().detect(r#"<!--#EXEC cmd="ls"-->"#).is_none());
137 assert!(det().detect("<!-- #exec cmd=\"ls\" -->").is_none());
138 }
139}