use std::sync::OnceLock;
use regex::RegexSet;
static SSI_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static SSI_DESCRIPTIONS: &[&str] = &[
"SSI exec directive (<!--#exec)",
"SSI include directive (<!--#include)",
"SSI config/echo/set directive",
];
fn patterns() -> &'static RegexSet {
SSI_PATTERNS.get_or_init(|| {
RegexSet::new([
r"(?i)<!--\s*#\s*exec\b",
r"(?i)<!--\s*#\s*include\b",
r"(?i)<!--\s*#\s*(config|echo|set|if|elif|else|endif|fsize|flastmod)\b",
])
.expect("SSI regex patterns must compile")
})
}
pub fn check_ssi(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(
SSI_DESCRIPTIONS
.get(idx)
.unwrap_or(&"SSI injection")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_exec() {
assert!(check_ssi(r#"<!--#exec cmd="id" -->"#).is_some());
assert!(check_ssi(r#"<!--#exec cgi="/cgi-bin/hack.cgi" -->"#).is_some());
}
#[test]
fn detects_include() {
assert!(check_ssi(r#"<!--#include virtual="/etc/passwd" -->"#).is_some());
}
#[test]
fn detects_config() {
assert!(check_ssi(r#"<!--#config timefmt="%Y" -->"#).is_some());
assert!(check_ssi(r#"<!--#echo var="DOCUMENT_ROOT" -->"#).is_some());
}
#[test]
fn allows_normal_html_comments() {
assert!(check_ssi("<!-- This is a comment -->").is_none());
assert!(check_ssi("<!-- TODO: fix this -->").is_none());
}
#[test]
fn allows_normal_text() {
assert!(check_ssi("Hello world").is_none());
}
}