use crate::{Filter, Operator, Value};
use std::fmt;
const MAX_VALUE_NODES: usize = 10000;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FilterValidator {
pub allowed_fields: Vec<String>,
pub denied_operators: Vec<Operator>,
pub max_depth: usize,
}
impl FilterValidator {
#[must_use]
pub fn new() -> Self {
Self {
allowed_fields: Vec::new(),
denied_operators: vec![crate::Operator::Regex],
max_depth: 5,
}
}
#[must_use]
pub const fn permissive() -> Self {
Self {
allowed_fields: Vec::new(),
denied_operators: Vec::new(),
max_depth: 5,
}
}
#[must_use]
pub fn allow_fields(mut self, fields: &[&str]) -> Self {
self.allowed_fields = fields.iter().map(|s| (*s).to_string()).collect();
self
}
#[must_use]
pub fn deny_operators(mut self, ops: &[Operator]) -> Self {
self.denied_operators = ops.to_vec();
self
}
#[must_use]
pub const fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn validate(&self, filter: &Filter) -> Result<(), ValidationError> {
self.validate_with_depth(filter, 0)
}
fn validate_with_depth(&self, filter: &Filter, depth: usize) -> Result<(), ValidationError> {
if depth > self.max_depth {
return Err(ValidationError::NestingTooDeep {
max: self.max_depth,
actual: depth,
});
}
if !self.allowed_fields.is_empty() && !self.allowed_fields.contains(&filter.field) {
return Err(ValidationError::FieldNotAllowed {
field: filter.field.clone(),
allowed: self.allowed_fields.clone(),
});
}
if self.denied_operators.contains(&filter.op) {
return Err(ValidationError::OperatorDenied {
operator: filter.op,
field: filter.field.clone(),
});
}
if let Value::Array(values) = &filter.value {
let mut node_count = 0;
for value in values {
self.validate_value_with_count(value, depth + 1, &mut node_count)?;
}
}
Ok(())
}
fn validate_value_with_count(
&self,
value: &Value,
depth: usize,
count: &mut usize,
) -> Result<(), ValidationError> {
*count += 1;
if *count > MAX_VALUE_NODES {
return Err(ValidationError::TooManyNodes {
max: MAX_VALUE_NODES,
});
}
if depth > self.max_depth {
return Err(ValidationError::NestingTooDeep {
max: self.max_depth,
actual: depth,
});
}
if let Value::Array(values) = value {
for v in values {
self.validate_value_with_count(v, depth + 1, count)?;
}
}
Ok(())
}
}
impl Default for FilterValidator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationError {
FieldNotAllowed {
field: String,
allowed: Vec<String>,
},
OperatorDenied {
operator: Operator,
field: String,
},
NestingTooDeep {
max: usize,
actual: usize,
},
TooManyNodes {
max: usize,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FieldNotAllowed { field, allowed } => {
if allowed.is_empty() {
write!(f, "field `{field}` is not allowed (no fields permitted)")
} else {
write!(
f,
"field `{field}` is not allowed; permitted: {}",
allowed.join(", ")
)
}
},
Self::OperatorDenied { operator, field } => {
let reason = match operator {
Operator::Regex => " (ReDoS prevention)",
_ => "",
};
write!(
f,
"operator `{operator:?}` denied for field `{field}`{reason}"
)
},
Self::NestingTooDeep { max, actual } => {
write!(
f,
"filter nesting depth {actual} exceeds maximum {max} (DoS prevention)"
)
},
Self::TooManyNodes { max } => {
write!(
f,
"filter contains too many value nodes (max {max}, DoS prevention)"
)
},
}
}
}
impl std::error::Error for ValidationError {}
pub fn merge_filters(
trusted: Vec<Filter>,
user: Vec<Filter>,
validator: &FilterValidator,
) -> Result<Vec<Filter>, ValidationError> {
for filter in &user {
validator.validate(filter)?;
}
let mut result = trusted;
result.extend(user);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validator_default_is_secure() {
let validator = FilterValidator::new();
assert!(validator.allowed_fields.is_empty());
assert_eq!(validator.denied_operators, vec![Operator::Regex]);
assert_eq!(validator.max_depth, 5);
}
#[test]
fn test_validator_permissive() {
let validator = FilterValidator::permissive();
assert!(validator.allowed_fields.is_empty());
assert!(validator.denied_operators.is_empty());
assert_eq!(validator.max_depth, 5);
}
#[test]
fn test_validator_builder() {
let validator = FilterValidator::new()
.allow_fields(&["name", "email"])
.deny_operators(&[Operator::Regex, Operator::ILike])
.max_depth(3);
assert_eq!(validator.allowed_fields.len(), 2);
assert_eq!(validator.denied_operators.len(), 2);
assert_eq!(validator.max_depth, 3);
}
#[test]
fn test_validate_allowed_field() {
let validator = FilterValidator::new().allow_fields(&["name", "email", "status"]);
let filter = Filter {
field: "name".into(),
op: Operator::Eq,
value: Value::String("Alice".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_validate_disallowed_field() {
let validator = FilterValidator::new().allow_fields(&["name", "email"]);
let filter = Filter {
field: "password".into(),
op: Operator::Eq,
value: Value::String("secret".into()),
};
let result = validator.validate(&filter);
assert!(result.is_err());
let ValidationError::FieldNotAllowed { field, allowed } = result.unwrap_err() else {
panic!("expected FieldNotAllowed, got different error variant")
};
assert_eq!(field, "password");
assert_eq!(allowed.len(), 2);
}
#[test]
fn test_validate_empty_whitelist_allows_all() {
let validator = FilterValidator::new();
let filter = Filter {
field: "any_field".into(),
op: Operator::Eq,
value: Value::String("value".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_validate_denied_operator() {
let validator = FilterValidator::new()
.allow_fields(&["name"])
.deny_operators(&[Operator::Regex, Operator::ILike]);
let filter = Filter {
field: "name".into(),
op: Operator::Regex,
value: Value::String("^A".into()),
};
let result = validator.validate(&filter);
assert!(result.is_err());
let ValidationError::OperatorDenied { operator, field } = result.unwrap_err() else {
panic!("expected OperatorDenied, got different error variant")
};
assert_eq!(operator, Operator::Regex);
assert_eq!(field, "name");
}
#[test]
fn test_validate_allowed_operator() {
let validator = FilterValidator::new()
.allow_fields(&["status"])
.deny_operators(&[Operator::Regex]);
let filter = Filter {
field: "status".into(),
op: Operator::Eq,
value: Value::String("active".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_validate_nesting_depth() {
let validator = FilterValidator::new().max_depth(2);
let filter = Filter {
field: "tags".into(),
op: Operator::In,
value: Value::Array(vec![Value::String("rust".into())]),
};
assert!(validator.validate(&filter).is_ok());
let filter_deep = Filter {
field: "deep".into(),
op: Operator::In,
value: Value::Array(vec![Value::Array(vec![Value::Array(vec![Value::String(
"too deep".into(),
)])])]),
};
let result = validator.validate(&filter_deep);
assert!(result.is_err());
let ValidationError::NestingTooDeep { max, actual } = result.unwrap_err() else {
panic!("expected NestingTooDeep, got different error variant")
};
assert_eq!(max, 2);
assert!(actual > max);
}
#[test]
fn test_merge_filters_success() {
let validator = FilterValidator::new().allow_fields(&["status", "name"]);
let trusted = vec![
Filter {
field: "org_id".into(),
op: Operator::Eq,
value: Value::Int(123),
},
Filter {
field: "deleted_at".into(),
op: Operator::Eq,
value: Value::Null,
},
];
let user = vec![Filter {
field: "status".into(),
op: Operator::Eq,
value: Value::String("active".into()),
}];
let result = merge_filters(trusted, user, &validator);
assert!(result.is_ok());
let filters = result.unwrap();
assert_eq!(filters.len(), 3);
assert_eq!(filters[0].field, "org_id");
assert_eq!(filters[1].field, "deleted_at");
assert_eq!(filters[2].field, "status");
}
#[test]
fn test_merge_filters_validation_error() {
let validator = FilterValidator::new().allow_fields(&["status"]);
let trusted = vec![Filter {
field: "org_id".into(),
op: Operator::Eq,
value: Value::Int(123),
}];
let user = vec![Filter {
field: "password".into(),
op: Operator::Eq,
value: Value::String("hack".into()),
}];
let result = merge_filters(trusted, user, &validator);
assert!(result.is_err());
}
#[test]
fn test_merge_filters_empty_user() {
let validator = FilterValidator::new();
let trusted = vec![Filter {
field: "org_id".into(),
op: Operator::Eq,
value: Value::Int(123),
}];
let user = vec![];
let result = merge_filters(trusted, user, &validator);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_merge_filters_empty_trusted() {
let validator = FilterValidator::new().allow_fields(&["name"]);
let trusted = vec![];
let user = vec![Filter {
field: "name".into(),
op: Operator::Eq,
value: Value::String("Alice".into()),
}];
let result = merge_filters(trusted, user, &validator);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_multiple_validation_errors() {
let validator = FilterValidator::new()
.allow_fields(&["status"])
.deny_operators(&[Operator::Regex]);
let filter1 = Filter {
field: "password".into(),
op: Operator::Eq,
value: Value::String("x".into()),
};
assert!(validator.validate(&filter1).is_err());
let filter2 = Filter {
field: "status".into(),
op: Operator::Regex,
value: Value::String("^A".into()),
};
assert!(validator.validate(&filter2).is_err());
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::FieldNotAllowed {
field: "password".into(),
allowed: vec!["name".into(), "email".into()],
};
let msg = format!("{err}");
assert!(msg.contains("password"));
assert!(msg.contains("name"));
let err = ValidationError::OperatorDenied {
operator: Operator::Regex,
field: "name".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Regex"));
assert!(msg.contains("name"));
let err = ValidationError::NestingTooDeep { max: 3, actual: 5 };
let msg = format!("{err}");
assert!(msg.contains('3'));
assert!(msg.contains('5'));
}
#[test]
fn test_in_operator_validation() {
let validator = FilterValidator::new().allow_fields(&["status"]);
let filter = Filter {
field: "status".into(),
op: Operator::In,
value: Value::Array(vec![
Value::String("active".into()),
Value::String("pending".into()),
]),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_not_in_operator_validation() {
let validator = FilterValidator::new()
.allow_fields(&["status"])
.deny_operators(&[Operator::NotIn]);
let filter = Filter {
field: "status".into(),
op: Operator::NotIn,
value: Value::Array(vec![Value::String("deleted".into())]),
};
assert!(validator.validate(&filter).is_err());
}
#[test]
fn test_null_value_validation() {
let validator = FilterValidator::new().allow_fields(&["deleted_at"]);
let filter = Filter {
field: "deleted_at".into(),
op: Operator::Eq,
value: Value::Null,
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_bool_value_validation() {
let validator = FilterValidator::new().allow_fields(&["active"]);
let filter = Filter {
field: "active".into(),
op: Operator::Eq,
value: Value::Bool(true),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_numeric_value_validation() {
let validator = FilterValidator::new().allow_fields(&["age", "price"]);
let filter1 = Filter {
field: "age".into(),
op: Operator::Gte,
value: Value::Int(18),
};
assert!(validator.validate(&filter1).is_ok());
let filter2 = Filter {
field: "price".into(),
op: Operator::Lt,
value: Value::Float(99.99),
};
assert!(validator.validate(&filter2).is_ok());
}
#[test]
fn test_new_denies_regex_by_default() {
let validator = FilterValidator::new().allow_fields(&["name"]);
let filter = Filter {
field: "name".into(),
op: Operator::Regex,
value: Value::String("^test".into()),
};
let result = validator.validate(&filter);
assert!(result.is_err());
let ValidationError::OperatorDenied { operator, .. } = result.unwrap_err() else {
panic!("expected OperatorDenied, got different error variant")
};
assert_eq!(operator, Operator::Regex);
}
#[test]
fn test_permissive_allows_regex() {
let validator = FilterValidator::permissive().allow_fields(&["name"]);
let filter = Filter {
field: "name".into(),
op: Operator::Regex,
value: Value::String("^test".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_new_allows_safe_operators() {
let validator = FilterValidator::new().allow_fields(&["name", "status"]);
let filter = Filter {
field: "status".into(),
op: Operator::Eq,
value: Value::String("active".into()),
};
assert!(validator.validate(&filter).is_ok());
let filter = Filter {
field: "name".into(),
op: Operator::Like,
value: Value::String("%test%".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_validate_compound_filter_deep_nesting() {
use crate::builder::{CompoundFilter, FilterExpr, simple};
let innermost = CompoundFilter::and(vec![
simple("a", Operator::Eq, Value::Int(1)),
simple("b", Operator::Eq, Value::Int(2)),
]);
let middle = CompoundFilter::or(vec![
FilterExpr::Compound(innermost),
simple("c", Operator::Eq, Value::Int(3)),
]);
let outer = CompoundFilter::and(vec![
FilterExpr::Compound(middle),
simple("d", Operator::Eq, Value::Int(4)),
]);
let validator = FilterValidator::new();
let simple_filter = Filter {
field: "a".into(),
op: Operator::Eq,
value: Value::Int(1),
};
assert!(validator.validate(&simple_filter).is_ok());
assert_eq!(outer.filters.len(), 2);
assert_eq!(outer.op, crate::LogicalOp::And);
}
#[test]
fn test_validate_compound_not_single_element() {
use crate::builder::{CompoundFilter, simple};
let not_filter = CompoundFilter::not(simple("deleted", Operator::Eq, Value::Bool(true)));
assert_eq!(not_filter.filters.len(), 1);
assert_eq!(not_filter.op, crate::LogicalOp::Not);
}
#[test]
fn test_validate_compound_empty_filters() {
use crate::builder::CompoundFilter;
let empty_and = CompoundFilter::and(vec![]);
let empty_or = CompoundFilter::or(vec![]);
assert!(empty_and.filters.is_empty());
assert!(empty_or.filters.is_empty());
}
#[test]
fn test_validate_deeply_nested_array_values() {
let validator = FilterValidator::new().max_depth(2);
let filter_ok = Filter {
field: "tags".into(),
op: Operator::In,
value: Value::Array(vec![Value::Array(vec![Value::Int(1)])]),
};
assert!(validator.validate(&filter_ok).is_ok());
let filter_too_deep = Filter {
field: "tags".into(),
op: Operator::In,
value: Value::Array(vec![Value::Array(vec![Value::Array(vec![Value::Int(1)])])]),
};
assert!(validator.validate(&filter_too_deep).is_err());
}
#[test]
fn test_filter_value_injection() {
let validator = FilterValidator::new().allow_fields(&["name", "email"]);
let filter = Filter {
field: "name".into(),
op: Operator::Eq,
value: Value::String("'; DROP TABLE users--".into()),
};
assert!(validator.validate(&filter).is_ok());
}
#[test]
fn test_filter_field_injection() {
let validator = FilterValidator::new().allow_fields(&["name", "email"]);
let filter = Filter {
field: "name; DROP TABLE users--".into(),
op: Operator::Eq,
value: Value::String("test".into()),
};
assert!(validator.validate(&filter).is_err());
}
#[test]
fn test_operator_based_attacks() {
let validator = FilterValidator::new()
.allow_fields(&["name"])
.deny_operators(&[Operator::Regex]);
let filter = Filter {
field: "name".into(),
op: Operator::Regex,
value: Value::String("^(a+)+$".into()), };
assert!(validator.validate(&filter).is_err());
let filter = Filter {
field: "name".into(),
op: Operator::Like,
value: Value::String("%test%".into()),
};
assert!(validator.validate(&filter).is_ok());
}
}