1use regex::RegexSet;
5use tracing::warn;
6use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
7
8use crate::{all_matches, body_str_values, Rule};
9
10pub static LFI_RFI_RULES: &[Rule] = &[
27 Rule {
28 id: "lfi-stream-wrapper",
29 pattern: r"(?i)(?:php|phar|zip|glob|expect|file|data)://",
31 severity: Severity::Critical,
32 paranoia: 1,
33 },
34 Rule {
35 id: "lfi-filter-chain",
36 pattern: r"(?i)convert\.(?:base64-encode|base64-decode|quoted-printable-encode)",
39 severity: Severity::Critical,
40 paranoia: 1,
41 },
42 Rule {
43 id: "lfi-data-base64",
44 pattern: r"(?i)data:[^,]*;base64,",
46 severity: Severity::Warning,
47 paranoia: 2,
48 },
49 Rule {
50 id: "rfi-remote-script",
51 pattern: r"(?i)(?:https?|ftp)://\S+\.(?:php|phtml|phar|asp|aspx|jsp)\b",
53 severity: Severity::Warning,
54 paranoia: 2,
55 },
56 Rule {
57 id: "rfi-remote-url",
58 pattern: r"(?i)(?:https?|ftp)://",
61 severity: Severity::Notice,
62 paranoia: 3,
63 },
64];
65
66#[derive(Default)]
69pub struct LfiRfiModule {
70 rule_set: Option<RegexSet>,
71 active_rules: Vec<&'static Rule>,
73}
74
75impl LfiRfiModule {
76 pub fn new() -> Self {
77 Self::default()
78 }
79}
80
81impl WafModule for LfiRfiModule {
82 fn id(&self) -> &str {
83 "lfi_rfi"
84 }
85
86 fn phase(&self) -> Phase {
87 Phase::Body
88 }
89
90 fn init(&mut self, cfg: &Config) {
91 let pl = cfg.waf.paranoia_level;
92 self.active_rules = LFI_RFI_RULES.iter().filter(|r| r.paranoia <= pl).collect();
93 self.rule_set = Some(
94 RegexSet::new(self.active_rules.iter().map(|r| r.pattern))
95 .expect("LFI/RFI rule compilation failed — check patterns at startup"),
96 );
97 }
98
99 fn inspect(&self, ctx: &RequestContext) -> Decision {
100 let Some(rule_set) = &self.rule_set else {
101 return Decision::Allow;
102 };
103
104 let query = ctx.normalized.query_params.iter().map(|(_, v)| v.as_str());
105 let cookies = ctx.normalized.cookies.iter().map(|(_, v)| v.as_str());
106 let body_vals = body_str_values(&ctx.normalized.body);
107 let body = body_vals.iter().map(String::as_str);
108 let derived = ctx.normalized.derived_decoded.iter().map(String::as_str);
109
110 let matched = all_matches(rule_set, query.chain(cookies).chain(body).chain(derived));
111 if matched.is_empty() {
112 return Decision::Allow;
113 }
114
115 let items: Vec<ScoreItem> = matched
116 .iter()
117 .map(|&idx| {
118 let rule = self.active_rules[idx];
119 warn!(
120 request_id = %ctx.request_id,
121 rule_id = %rule.id,
122 severity = ?rule.severity,
123 "lfi/rfi detection"
124 );
125 ScoreItem {
126 rule_id: rule.id.to_string(),
127 severity: rule.severity,
128 }
129 })
130 .collect();
131
132 Decision::Scores(items)
133 }
134}