pub mod body;
pub mod graphql;
pub mod grpc;
pub mod url;
use waf_core::{LimitsConfig, RequestContext};
use crate::body::parse_body;
use crate::url::{canonicalize_value, normalize_path, parse_cookies_limited, parse_query};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NormalizationError {
BodyTooLarge { limit: usize, actual: usize },
TooManyHeaders { limit: usize },
HeaderTooLarge { limit: usize, actual: usize },
TooManyParams { limit: usize },
TooManyCookies { limit: usize },
JsonDepthExceeded { limit: usize },
JsonParseError(String),
MultipartError(String),
}
impl std::fmt::Display for NormalizationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BodyTooLarge { limit, actual } =>
write!(f, "body too large: {actual} bytes (limit {limit})"),
Self::TooManyHeaders { limit } =>
write!(f, "too many headers (limit {limit})"),
Self::HeaderTooLarge { limit, actual } =>
write!(f, "header value too large: {actual} bytes (limit {limit})"),
Self::TooManyParams { limit } =>
write!(f, "too many parameters (limit {limit})"),
Self::TooManyCookies { limit } =>
write!(f, "too many cookies (limit {limit})"),
Self::JsonDepthExceeded { limit } =>
write!(f, "JSON nesting exceeds depth limit {limit}"),
Self::JsonParseError(msg) =>
write!(f, "JSON parse error: {msg}"),
Self::MultipartError(msg) =>
write!(f, "multipart error: {msg}"),
}
}
}
pub struct Normalizer {
limits: LimitsConfig,
}
impl Normalizer {
pub fn new(limits: &LimitsConfig) -> Self {
Self { limits: limits.clone() }
}
pub fn normalize(&self, ctx: &mut RequestContext) -> Result<(), NormalizationError> {
let limits = &self.limits;
let body_len = ctx.body.len();
if body_len > limits.max_body_size {
return Err(NormalizationError::BodyTooLarge {
limit: limits.max_body_size,
actual: body_len,
});
}
if ctx.headers.len() > limits.max_headers {
return Err(NormalizationError::TooManyHeaders { limit: limits.max_headers });
}
for (_name, value) in &ctx.headers {
if value.len() > limits.max_header_size {
return Err(NormalizationError::HeaderTooLarge {
limit: limits.max_header_size,
actual: value.len(),
});
}
}
let norm_headers: Vec<(String, String)> = ctx
.headers
.iter()
.map(|(k, v)| (k.to_lowercase(), v.trim().to_string()))
.collect();
let mut derived: Vec<String> = Vec::new();
let mut cookies = Vec::new();
let mut cookie_double_enc = false;
for (name, value) in &norm_headers {
if name == "cookie" {
for (k, v) in parse_cookies_limited(value, limits.max_cookies)? {
let (dk, de_k) = canonicalize_value(&k, false);
let (dv, de_v) = canonicalize_value(&v, false);
cookie_double_enc |= de_k || de_v;
cookies.push((dk, dv)); }
}
}
let (norm_path, path_double_enc) = normalize_path(&ctx.raw_path);
for raw_seg in ctx.raw_path.split('/').filter(|s| !s.is_empty()) {
let (seg, _) = url::percent_decode(raw_seg, false);
derived.extend(url::derive_variants(&seg));
}
let (query_params, query_double_enc) = match &ctx.query {
Some(q) => {
let (p, de, d) = parse_query(q, limits)?;
derived.extend(d);
(p, de)
}
None => (Vec::new(), false),
};
let content_type = norm_headers
.iter()
.find(|(k, _)| k == "content-type")
.map(|(_, v)| v.as_str());
let parsed_body = parse_body(content_type, &ctx.body, limits)?;
match &parsed_body {
waf_core::ParsedBody::JsonFlattened(pairs) => {
for (_, v) in pairs {
derived.extend(url::json_leaf_derived(v));
}
}
waf_core::ParsedBody::Raw(b) => {
let is_grpc = content_type
.map(|ct| ct.trim_start().starts_with("application/grpc"))
.unwrap_or(false);
if is_grpc {
let ex = crate::grpc::grpc_extract(b, crate::grpc::GrpcLimits::default());
for leaf in ex.leaves {
let canonical = url::canonicalize_value(&leaf, false).0;
derived.extend(url::derive_variants(&canonical));
derived.push(canonical);
}
} else if let Ok(raw) = std::str::from_utf8(b) {
let canonical = url::canonicalize_value(raw, false).0;
derived.extend(url::derive_variants(&canonical));
if canonical != raw {
derived.push(canonical);
}
}
}
other => {
for s in body_canonical_strings(other) {
derived.extend(url::derive_variants(&s));
}
}
}
for (name, value) in &norm_headers {
if !header_base64_excluded(name) {
derived.extend(url::derive_variants(value));
}
}
ctx.normalized.path = norm_path;
ctx.normalized.query = ctx.query.clone();
ctx.normalized.query_params = query_params;
ctx.normalized.cookies = cookies;
ctx.normalized.headers = norm_headers;
ctx.normalized.body = parsed_body;
ctx.normalized.double_encoding_detected =
path_double_enc || query_double_enc || cookie_double_enc;
ctx.normalized.derived_decoded = derived;
Ok(())
}
}
fn header_base64_excluded(name: &str) -> bool {
const EXCLUDED: &[&str] = &[
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"etag",
"if-none-match",
"if-match",
];
EXCLUDED.contains(&name) || name.ends_with("-token")
}
pub fn header_content_inspectable(name: &str) -> bool {
const DENY_EXACT: &[&str] = &[
"authorization", "proxy-authorization", "cookie", "set-cookie",
"user-agent", "host", "etag", "if-none-match", "if-match",
"connection", "keep-alive", "transfer-encoding", "te", "trailer", "upgrade",
];
if DENY_EXACT.contains(&name)
|| name.ends_with("-token")
|| name.starts_with("accept")
|| name.starts_with("content-")
|| name.starts_with("proxy-")
{
return false;
}
matches!(name, "referer" | "x-forwarded-for" | "x-forwarded-host" | "x-forwarded-proto")
|| name.starts_with("x-")
}
fn body_canonical_strings(body: &waf_core::ParsedBody) -> Vec<String> {
use waf_core::ParsedBody;
match body {
ParsedBody::FormUrlEncoded(p) => p.iter().map(|(_, v)| v.clone()).collect(),
ParsedBody::JsonFlattened(p) => {
p.iter().map(|(_, v)| url::canonicalize_value(v, false).0).collect()
}
ParsedBody::Multipart(fields) => {
let mut out = Vec::with_capacity(fields.len() * 2);
for f in fields {
out.push(url::canonicalize_multipart_field(&f.name));
if let Some(fname) = &f.filename {
out.push(url::canonicalize_multipart_field(fname));
}
if let Ok(s) = std::str::from_utf8(&f.data) {
out.push(url::canonicalize_multipart_field(s));
}
}
out
}
ParsedBody::Raw(b) => {
std::str::from_utf8(b).ok().map(|s| url::canonicalize_value(s, false).0).into_iter().collect()
}
ParsedBody::None => Vec::new(),
}
}