3 releases
| new 0.1.2 | Apr 10, 2026 |
|---|---|
| 0.1.1 | Feb 19, 2026 |
| 0.1.0 | Feb 17, 2026 |
#1337 in Network programming
290KB
6.5K
SLoC
zentinel-modsec
Pure Rust ModSecurity implementation with full OWASP CRS compatibility.
A complete ModSecurity rule engine written in Rust with zero C/C++ dependencies. Load and execute OWASP Core Rule Set (CRS) rules for web application firewall (WAF) functionality in any Rust application.
Performance: 10-30x Faster than libmodsecurity
| Benchmark | zentinel-modsec | libmodsecurity (C++) | Speedup |
|---|---|---|---|
| Clean request | 161 ns | 4,831 ns | 30x faster |
| SQLi detection | 295 ns | 5,545 ns | 19x faster |
| Body processing | 1.24 µs | 12.93 µs | 10x faster |
| Rule parsing | 2.75 µs | 10.07 µs | 3.6x faster |
| Throughput | 6.2M req/s | 207K req/s | 30x higher |
Features
- Full OWASP CRS Compatibility - Parse and execute 800+ CRS rules
- Pure Rust - No libmodsecurity, no C/C++ dependencies, no FFI
- SecLang Support - Load standard ModSecurity
.confrule files - Built-in Detection - Native
@detectSQLiand@detectXSSoperators (pure Rust libinjection) - All Operators -
@rx,@pm,@pmFromFile,@contains,@streq,@ipMatch, and 30+ more - All Transformations -
t:lowercase,t:urlDecode,t:base64Decode,t:htmlEntityDecode, and 30+ more - Thread-Safe -
Send + Sync, safe for concurrent request processing - Async-Ready - Works with tokio, async-std, or any async runtime
- Zero Unsafe -
#![deny(unsafe_code)]
Quick Start
Add to your Cargo.toml:
[dependencies]
zentinel-modsec = "0.1"
Basic Usage
use zentinel_modsec::{ModSecurity, Rules, Transaction};
fn main() -> zentinel_modsec::Result<()> {
// Create the ModSecurity engine
let modsec = ModSecurity::new();
// Load rules
let mut rules = Rules::new();
rules.add_plain("SecRuleEngine On")?;
rules.add_plain(r#"
SecRule REQUEST_URI "@contains /admin" \
"id:1,phase:1,deny,status:403,msg:'Admin access blocked'"
"#)?;
// Compile rules (do this once, reuse for all requests)
let ruleset = rules.compile()?;
// Process a request
let mut tx = modsec.transaction(&ruleset);
tx.process_uri("/admin/dashboard", "GET", "HTTP/1.1")?;
tx.add_request_header("Host", "example.com")?;
tx.add_request_header("User-Agent", "Mozilla/5.0")?;
tx.process_request_headers()?;
// Check for intervention (block/redirect/etc)
if let Some(intervention) = tx.intervention() {
println!("Blocked: status={}, rule={:?}",
intervention.status,
intervention.rule_id);
}
Ok(())
}
Loading OWASP CRS Rules
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
// Load CRS setup
rules.add_file("/etc/modsecurity/crs/crs-setup.conf")?;
// Load all CRS rules (glob patterns supported)
rules.add_file("/etc/modsecurity/crs/rules/*.conf")?;
let ruleset = rules.compile()?;
println!("Loaded {} rules", ruleset.rule_count());
Ok(())
}
SQL Injection Detection
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
rules.add_plain(r#"
SecRuleEngine On
SecRule ARGS "@detectSQLi" \
"id:942100,phase:2,deny,status:403,msg:'SQL Injection detected'"
"#)?;
let ruleset = rules.compile()?;
let mut tx = modsec.transaction(&ruleset);
// Simulate a request with SQLi payload
tx.process_uri("/search?q=' OR 1=1--", "GET", "HTTP/1.1")?;
tx.process_request_headers()?;
assert!(tx.has_intervention());
println!("SQLi attack blocked!");
Ok(())
}
XSS Detection
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
rules.add_plain(r#"
SecRuleEngine On
SecRule ARGS "@detectXSS" \
"id:941100,phase:2,deny,status:403,msg:'XSS detected'"
"#)?;
let ruleset = rules.compile()?;
let mut tx = modsec.transaction(&ruleset);
tx.process_uri("/comment?text=<script>alert(1)</script>", "GET", "HTTP/1.1")?;
tx.process_request_headers()?;
assert!(tx.has_intervention());
println!("XSS attack blocked!");
Ok(())
}
Request Body Inspection
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
rules.add_plain(r#"
SecRuleEngine On
SecRequestBodyAccess On
SecRule REQUEST_BODY "@detectSQLi" \
"id:942110,phase:2,deny,status:403,msg:'SQLi in body'"
"#)?;
let ruleset = rules.compile()?;
let mut tx = modsec.transaction(&ruleset);
tx.process_uri("/api/login", "POST", "HTTP/1.1")?;
tx.add_request_header("Content-Type", "application/x-www-form-urlencoded")?;
tx.process_request_headers()?;
// Add request body
tx.append_request_body(b"username=admin&password=' OR 1=1--")?;
tx.process_request_body()?;
assert!(tx.has_intervention());
Ok(())
}
Detection-Only Mode
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
rules.add_plain(r#"
SecRuleEngine DetectionOnly
SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny"
"#)?;
let ruleset = rules.compile()?;
let mut tx = modsec.transaction(&ruleset);
tx.process_uri("/admin", "GET", "HTTP/1.1")?;
tx.process_request_headers()?;
// Rule matched but no intervention (detection only)
assert!(!tx.has_intervention());
assert!(tx.matched_rules().contains(&"1".to_string()));
println!("Detected but not blocked: {:?}", tx.matched_rules());
Ok(())
}
Anomaly Scoring
use zentinel_modsec::{ModSecurity, Rules};
fn main() -> zentinel_modsec::Result<()> {
let modsec = ModSecurity::new();
let mut rules = Rules::new();
rules.add_plain(r#"
SecRuleEngine On
# Increment score for suspicious patterns
SecRule REQUEST_URI "@contains /admin" \
"id:1,phase:1,pass,setvar:'TX.anomaly_score=+5'"
SecRule REQUEST_HEADERS:User-Agent "@contains sqlmap" \
"id:2,phase:1,pass,setvar:'TX.anomaly_score=+10'"
# Block if score exceeds threshold
SecRule TX:anomaly_score "@ge 10" \
"id:100,phase:1,deny,status:403,msg:'Anomaly score exceeded'"
"#)?;
let ruleset = rules.compile()?;
let mut tx = modsec.transaction(&ruleset);
tx.process_uri("/admin", "GET", "HTTP/1.1")?;
tx.add_request_header("User-Agent", "sqlmap/1.0")?;
tx.process_request_headers()?;
println!("Anomaly score: {}", tx.anomaly_score());
assert!(tx.has_intervention());
Ok(())
}
Framework Integration
Axum
use axum::{
body::Body,
extract::State,
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
routing::get,
Router,
};
use zentinel_modsec::{ModSecurity, CompiledRuleset};
use std::sync::Arc;
async fn waf_middleware(
State(ruleset): State<Arc<CompiledRuleset>>,
request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
let modsec = ModSecurity::new();
let mut tx = modsec.transaction(&ruleset);
// Process request
tx.process_uri(
request.uri().path_and_query().map(|pq| pq.as_str()).unwrap_or("/"),
request.method().as_str(),
"HTTP/1.1",
).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
for (name, value) in request.headers() {
if let Ok(v) = value.to_str() {
let _ = tx.add_request_header(name.as_str(), v);
}
}
tx.process_request_headers()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Check for intervention
if let Some(intervention) = tx.intervention() {
return Err(StatusCode::from_u16(intervention.status).unwrap_or(StatusCode::FORBIDDEN));
}
Ok(next.run(request).await)
}
#[tokio::main]
async fn main() {
let mut rules = zentinel_modsec::Rules::new();
rules.add_file("/etc/modsecurity/crs/rules/*.conf").unwrap();
let ruleset = Arc::new(rules.compile().unwrap());
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.layer(middleware::from_fn_with_state(ruleset.clone(), waf_middleware))
.with_state(ruleset);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Actix-web
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, middleware};
use zentinel_modsec::{ModSecurity, CompiledRuleset};
use std::sync::Arc;
async fn waf_check(
req: HttpRequest,
ruleset: web::Data<Arc<CompiledRuleset>>,
) -> Option<HttpResponse> {
let modsec = ModSecurity::new();
let mut tx = modsec.transaction(&ruleset);
tx.process_uri(req.uri().path_and_query().map(|pq| pq.as_str()).unwrap_or("/"),
req.method().as_str(),
"HTTP/1.1").ok()?;
for (name, value) in req.headers() {
if let Ok(v) = value.to_str() {
let _ = tx.add_request_header(name.as_str(), v);
}
}
tx.process_request_headers().ok()?;
tx.intervention().map(|i| {
HttpResponse::build(actix_web::http::StatusCode::from_u16(i.status).unwrap())
.body(format!("Blocked by rule: {:?}", i.rule_id))
})
}
Supported SecLang Directives
Directives
| Directive | Status | Description |
|---|---|---|
SecRule |
✅ | Main rule directive |
SecAction |
✅ | Unconditional action |
SecMarker |
✅ | Named marker for skipAfter |
SecRuleEngine |
✅ | On/Off/DetectionOnly |
SecRequestBodyAccess |
✅ | Enable body inspection |
SecResponseBodyAccess |
✅ | Enable response inspection |
Include |
✅ | Include other rule files |
Operators
| Operator | Status | Description |
|---|---|---|
@rx |
✅ | Regular expression |
@pm |
✅ | Phrase match (Aho-Corasick) |
@pmFromFile |
✅ | Phrase match from file |
@contains |
✅ | String contains |
@streq |
✅ | String equals |
@beginsWith |
✅ | String begins with |
@endsWith |
✅ | String ends with |
@within |
✅ | Value within list |
@eq, @ne, @gt, @ge, @lt, @le |
✅ | Numeric comparison |
@detectSQLi |
✅ | SQL injection detection |
@detectXSS |
✅ | XSS detection |
@ipMatch |
✅ | IP/CIDR matching |
@validateUrlEncoding |
✅ | URL encoding validation |
@validateUtf8Encoding |
✅ | UTF-8 validation |
Transformations
| Transformation | Status | Description |
|---|---|---|
t:lowercase |
✅ | Convert to lowercase |
t:uppercase |
✅ | Convert to uppercase |
t:urlDecode |
✅ | URL decode |
t:urlDecodeUni |
✅ | URL decode (Unicode) |
t:base64Decode |
✅ | Base64 decode |
t:base64Encode |
✅ | Base64 encode |
t:htmlEntityDecode |
✅ | HTML entity decode |
t:removeWhitespace |
✅ | Remove whitespace |
t:compressWhitespace |
✅ | Compress whitespace |
t:normalizePath |
✅ | Normalize path |
t:normalizePathWin |
✅ | Normalize Windows path |
t:cmdLine |
✅ | Command line normalization |
t:md5 |
✅ | MD5 hash |
t:sha1 |
✅ | SHA1 hash |
t:hexEncode |
✅ | Hex encode |
t:hexDecode |
✅ | Hex decode |
Actions
| Action | Status | Description |
|---|---|---|
deny |
✅ | Block request |
block |
✅ | Block with default status |
pass |
✅ | Continue processing |
allow |
✅ | Skip remaining rules |
redirect |
✅ | Redirect to URL |
drop |
✅ | Drop connection |
chain |
✅ | Chain to next rule |
skip |
✅ | Skip N rules |
skipAfter |
✅ | Skip to marker |
setvar |
✅ | Set variable |
capture |
✅ | Capture regex groups |
id |
✅ | Rule ID |
phase |
✅ | Processing phase |
severity |
✅ | Severity level |
msg |
✅ | Log message |
tag |
✅ | Rule tag |
Why Pure Rust?
- Performance - 10-30x faster than C++ libmodsecurity
- Safety - Memory safety guaranteed, no buffer overflows
- Portability - Runs anywhere Rust compiles (including WASM)
- Simplicity -
cargo add zentinel-modsec, no system dependencies - Auditability - Single-language codebase, easier security review
Technical Optimizations
- PHF (Perfect Hash Functions) - O(1) operator/variable lookup
- Lazy Regex Compilation - Defer compilation to first use
- Aho-Corasick - O(n) multi-pattern matching for
@pm - RegexSet - Single-pass multi-regex evaluation for XSS detection
- Zero-Copy Parsing -
Cow<str>avoids allocations when possible - No FFI Overhead - Pure Rust, no cross-language calls
OWASP CRS Setup
# Download OWASP Core Rule Set
git clone https://github.com/coreruleset/coreruleset /etc/modsecurity/crs
cp /etc/modsecurity/crs/crs-setup.conf.example /etc/modsecurity/crs/crs-setup.conf
# Use in your application
rules.add_file("/etc/modsecurity/crs/crs-setup.conf")?;
rules.add_file("/etc/modsecurity/crs/rules/*.conf")?;
Comparison
| Feature | zentinel-modsec | libmodsecurity | mod_security |
|---|---|---|---|
| Language | Pure Rust | C++ | C |
| Dependencies | None | PCRE, libxml2, etc. | Apache/nginx |
| Performance | 6.2M req/s | 207K req/s | ~200K req/s |
| CRS Compatible | ✅ | ✅ | ✅ |
| WASM Support | ✅ | ❌ | ❌ |
| Memory Safety | ✅ Guaranteed | ❌ Manual | ❌ Manual |
License
Apache-2.0
Contributing
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
Related Projects
- Zentinel - Extensible reverse proxy using this engine
- OWASP CRS - Core Rule Set for ModSecurity
- libmodsecurity - Original C++ implementation
Dependencies
~10–15MB
~201K SLoC