use regex::RegexSet;
use tracing::warn;
use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
use crate::{all_matches, body_str_values, inspectable_header_values, Rule};
pub static RCE_RULES: &[Rule] = &[
Rule {
id: "rce-cmd-substitution",
pattern: r"\$\([^)]+\)",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-chained-command",
pattern: r"(?i)[;&|]\s*(?:cat|ls|id|whoami|uname|pwd|wget|curl|nc|ncat|netcat|ping|bash|sh|zsh|python|perl|ruby|nslookup|dig|getent|host|chmod|rm|cp|mv|kill|telnet|powershell|cmd)\b",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-shell-path",
pattern: r"(?i)(?:/bin/(?:sh|bash|zsh|dash|ksh)\b|cmd\.exe|powershell\.exe)",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-reverse-shell",
pattern: r"(?i)(?:/dev/tcp/|bash\s+-i|nc\s+-[a-z]*e|mkfifo)",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-yaml-deserialization",
pattern: r"(?i)!!python/(?:object|module|name|apply)",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-backtick",
pattern: r"`[^`]+`",
severity: Severity::Warning,
paranoia: 2,
},
Rule {
id: "rce-ifs-evasion",
pattern: r"(?i)\$(?:\{ifs\}|ifs\b)",
severity: Severity::Warning,
paranoia: 2,
},
Rule {
id: "rce-download-exec",
pattern: r"(?i)\b(?:wget|curl)\s+(?:-\S+\s+)*https?://",
severity: Severity::Warning,
paranoia: 2,
},
Rule {
id: "rce-windows-shell",
pattern: r"(?i)(?:cmd(?:\.exe)?\s*/c|powershell(?:\.exe)?\s+-|\bset\s+/[ap]\b)",
severity: Severity::Warning,
paranoia: 2,
},
Rule {
id: "rce-logical-operator",
pattern: r"&&|\|\|",
severity: Severity::Notice,
paranoia: 3,
},
Rule {
id: "rce-expression-language",
pattern: r"(?i)[#$]\{[^}]*\b(?:print|eval|exec|system|passthru|assert|shell_exec|popen|phpinfo|file_get_contents|getruntime|runtime|processbuilder)\s*\(",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-vbscript-on-error",
pattern: r"(?i)\bon\s+error\s+resume\s+next\b",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-asp-server-intrinsic",
pattern: r"(?i)\bserver\.(?:scripttimeout|createobject|mappath|execute|transfer)\b",
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-vbscript-createobject",
pattern: r#"(?i)\bcreateobject\s*\(\s*["']?(?:wscript\.|msxml|adodb\.|scripting\.filesystemobject|shell\.application|microsoft\.xmlhttp)"#,
severity: Severity::Critical,
paranoia: 1,
},
Rule {
id: "rce-asp-response-write",
pattern: r"(?i)\bresponse\.write\s*\(",
severity: Severity::Warning,
paranoia: 2,
},
];
#[derive(Default)]
pub struct RceModule {
rule_set: Option<RegexSet>,
active_rules: Vec<&'static Rule>,
}
impl RceModule {
pub fn new() -> Self {
Self::default()
}
}
impl WafModule for RceModule {
fn id(&self) -> &str {
"rce"
}
fn phase(&self) -> Phase {
Phase::Body
}
fn init(&mut self, cfg: &Config) {
let pl = cfg.waf.paranoia_level;
self.active_rules = RCE_RULES.iter().filter(|r| r.paranoia <= pl).collect();
self.rule_set = Some(
RegexSet::new(self.active_rules.iter().map(|r| r.pattern))
.expect("RCE rule compilation failed — check patterns at startup"),
);
}
fn inspect(&self, ctx: &RequestContext) -> Decision {
let Some(rule_set) = &self.rule_set else {
return Decision::Allow;
};
let path = std::iter::once(ctx.normalized.path.as_str());
let query = ctx.normalized.query_params.iter().map(|(_, v)| v.as_str());
let cookies = ctx.normalized.cookies.iter().map(|(_, v)| v.as_str());
let body_vals = body_str_values(&ctx.normalized.body);
let body = body_vals.iter().map(String::as_str);
let derived = ctx.normalized.derived_decoded.iter().map(String::as_str);
let headers = inspectable_header_values(ctx);
let matched = all_matches(rule_set, path.chain(query).chain(cookies).chain(body).chain(derived).chain(headers));
if matched.is_empty() {
return Decision::Allow;
}
let items: Vec<ScoreItem> = matched
.iter()
.map(|&idx| {
let rule = self.active_rules[idx];
warn!(
request_id = %ctx.request_id,
rule_id = %rule.id,
severity = ?rule.severity,
"rce detection"
);
ScoreItem {
rule_id: rule.id.to_string(),
severity: rule.severity,
}
})
.collect();
Decision::Scores(items)
}
}