use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;
static STRONG_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"<!--#exec cmd=",
r#"|<!--#include file=["']?(?:/|\.\.)"#,
r"|<!--#printenv",
))
.unwrap()
});
static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"<!--#include file=",
r"|<!--#echo var=",
r"|<!--#fsize",
r"|<!--#flastmod",
r"|<!--#config",
))
.unwrap()
});
pub struct SsiInjectionDetector;
impl Detector for SsiInjectionDetector {
fn name(&self) -> &'static str {
"ssi_injection"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
std::slice::from_ref(&*STRONG_PATTERNS),
self.name(),
AttackCategory::Injection,
Severity::High,
"SSI Server-Side Include injection detected",
input,
)
.or_else(|| {
regex_detect(
std::slice::from_ref(&*WEAK_PATTERNS),
self.name(),
AttackCategory::Injection,
Severity::Low,
"SSI directive present (weak signal)",
input,
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det() -> SsiInjectionDetector {
SsiInjectionDetector
}
fn assert_hit_at(input: &str, severity: Severity) {
crate::test_helpers::assert_detected(&det(), input, AttackCategory::Injection, severity);
}
#[test]
fn name_is_ssi_injection() {
assert_eq!(det().name(), "ssi_injection");
}
#[test]
fn detects_common_payloads() {
for (input, severity) in [
(r#"<!--#exec cmd="cat /etc/passwd"-->"#, Severity::High),
(r#"<!--#include file="/etc/passwd"-->"#, Severity::High),
(r#"<!--#include file="../../etc/passwd"-->"#, Severity::High),
(r#"<!--#printenv-->"#, Severity::High),
(r#"<!--#echo var="DATE_LOCAL"-->"#, Severity::Low),
(r#"<!--#fsize file="index.html"-->"#, Severity::Low),
(r#"<!--#flastmod file="index.html"-->"#, Severity::Low),
(r#"<!--#config timefmt="%B"-->"#, Severity::Low),
] {
assert_hit_at(input, severity);
}
}
#[test]
fn stock_ssi_page_is_not_high() {
let page = "<!--#include file=\"header.html\" -->\n\
<!--#echo var=\"DATE_LOCAL\" -->\n\
<!--#config timefmt=\"%Y\" -->\n\
<!--#fsize file=\"footer.html\" -->";
assert_hit_at(page, Severity::Low);
assert!(det().detect(page).is_some());
}
#[test]
fn benign_inputs_not_detected() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"<!-- this is a plain comment -->",
"The page was generated at 3:00 PM",
"Include the file below the table",
"The include directive resolves the path at build time.",
] {
assert!(det().detect(input).is_none(), "false positive: {input}");
}
}
#[test]
fn edge_cases() {
assert!(det().detect("").is_none());
assert!(det().detect(" \t\n ").is_none());
assert!(det().detect("你好世界 こんにちは").is_none());
assert!(det().detect("<!--#exec").is_none());
assert!(det().detect(r#"<!--#EXEC cmd="ls"-->"#).is_none());
assert!(det().detect("<!-- #exec cmd=\"ls\" -->").is_none());
}
}