pub mod crs;
pub mod graphql;
pub mod grpc;
pub mod header_injection;
pub mod ldap;
pub mod lfi_rfi;
pub mod mail;
pub mod nosql;
pub mod path_traversal;
pub mod rate_limit;
pub mod rce;
pub mod request_smuggling;
pub mod scanner;
pub mod sqli;
pub mod ssi;
pub mod ssrf;
pub mod ssti;
pub mod xss;
pub mod xxe;
use regex::RegexSet;
use waf_core::{ParsedBody, RequestContext, Severity};
use waf_normalizer::url::canonicalize_multipart_field;
pub const HIGHEST_RULE_PARANOIA: u8 = 3;
pub struct Rule {
pub id: &'static str,
pub pattern: &'static str,
pub severity: Severity,
pub paranoia: u8,
}
pub(crate) fn all_matches(
rule_set: ®ex::RegexSet,
values: impl Iterator<Item = impl AsRef<str>>,
) -> Vec<usize> {
let mut matched = vec![false; rule_set.len()];
for v in values {
for idx in rule_set.matches(v.as_ref()).into_iter() {
matched[idx] = true;
}
}
matched
.iter()
.enumerate()
.filter_map(|(i, &hit)| if hit { Some(i) } else { None })
.collect()
}
pub type RuleList = Vec<(&'static str, &'static str)>;
pub fn content_rules_split(paranoia: u8) -> (RuleList, RuleList) {
let mut main: RuleList = Vec::new();
let mut host: RuleList = Vec::new();
for table in [
sqli::SQLI_RULES,
xss::XSS_RULES,
path_traversal::PATH_TRAVERSAL_RULES,
rce::RCE_RULES,
lfi_rfi::LFI_RFI_RULES,
ssrf::SSRF_RULES,
ldap::LDAP_RULES,
nosql::NOSQL_RULES,
mail::MAIL_RULES,
ssti::SSTI_RULES,
scanner::SCANNER_RULES,
ssi::SSI_RULES,
xxe::XXE_RULES,
] {
for r in table.iter().filter(|r| r.paranoia <= paranoia) {
main.push((r.id, r.pattern));
}
}
for (id, pattern, par, host_only) in header_injection::rule_meta() {
if par <= paranoia {
if host_only {
host.push((id, pattern));
} else {
main.push((id, pattern));
}
}
}
(main, host)
}
pub struct ContentPrefilter {
main: RegexSet,
host: RegexSet,
main_ids: Vec<&'static str>,
host_ids: Vec<&'static str>,
}
impl ContentPrefilter {
pub fn new(paranoia: u8) -> Self {
let (main_rules, host_rules) = content_rules_split(paranoia);
let main = RegexSet::new(main_rules.iter().map(|(_, p)| *p))
.expect("content prefilter MAIN union compilation failed");
let host = RegexSet::new(host_rules.iter().map(|(_, p)| *p))
.expect("content prefilter HOST union compilation failed");
Self {
main,
host,
main_ids: main_rules.iter().map(|(id, _)| *id).collect(),
host_ids: host_rules.iter().map(|(id, _)| *id).collect(),
}
}
pub fn rule_ids(&self) -> Vec<&'static str> {
self.main_ids.iter().chain(&self.host_ids).copied().collect()
}
pub fn host_rule_ids(&self) -> &[&'static str] {
&self.host_ids
}
pub fn is_candidate(&self, ctx: &RequestContext) -> bool {
let n = &ctx.normalized;
if self.main.is_match(&n.path) {
return true;
}
for (_, v) in n.query_params.iter().chain(&n.cookies).chain(&n.headers) {
if self.main.is_match(v) {
return true;
}
}
if body_str_values(&n.body).iter().any(|v| self.main.is_match(v)) {
return true;
}
if n.derived_decoded.iter().any(|v| self.main.is_match(v)) {
return true;
}
n.headers
.iter()
.filter(|(name, _)| name == "host" || name == "x-forwarded-host")
.any(|(_, v)| self.host.is_match(v))
}
}
pub(crate) fn inspectable_header_values(ctx: &RequestContext) -> impl Iterator<Item = &str> {
ctx.normalized
.headers
.iter()
.filter(|(name, _)| waf_normalizer::header_content_inspectable(name))
.map(|(_, v)| v.as_str())
}
pub(crate) fn body_str_values(body: &ParsedBody) -> Vec<String> {
match body {
ParsedBody::FormUrlEncoded(params) => {
params.iter().map(|(_, v)| v.clone()).collect()
}
ParsedBody::JsonFlattened(pairs) => {
pairs.iter().map(|(_, v)| v.clone()).collect()
}
ParsedBody::Multipart(fields) => {
let mut out = Vec::with_capacity(fields.len() * 2);
for f in fields {
out.push(canonicalize_multipart_field(&f.name));
if let Some(filename) = &f.filename {
out.push(canonicalize_multipart_field(filename));
}
if let Ok(s) = std::str::from_utf8(&f.data) {
out.push(canonicalize_multipart_field(s));
}
}
out
}
ParsedBody::Raw(bytes) => {
match std::str::from_utf8(bytes) {
Ok(s) if !bytes.contains(&0) => vec![s.to_owned()],
_ => vec![],
}
}
ParsedBody::None => vec![],
}
}