Most web application firewalls work like antivirus software. They maintain massive rule databases of known attack signatures and try to match incoming requests against them. The NAXSI WAF takes the opposite approach entirely.
NAXSI (NGINX Anti XSS & SQL Injection) is a lightweight, high-performance NAXSI WAF module for NGINX. Instead of relying on thousands of regularly updated signatures, the NAXSI WAF ships with a small set of simple rules. These rules flag suspicious patterns β characters like <, ', SELECT, and .. that rarely appear in legitimate requests. You then whitelist the specific parameters and URLs where those characters are expected.
This whitelist-based approach means the NAXSI WAF can block unknown zero-day attacks that signature-based firewalls miss. If an attack uses suspicious characters (and virtually all do), NAXSI catches it β no signature update required.
In this guide, you will learn how to install, configure, and deploy the NAXSI WAF on NGINX for Rocky Linux, AlmaLinux, and RHEL systems.
NAXSI vs. ModSecurity: Why Choose the NAXSI WAF?
Before diving into setup, it helps to understand where NAXSI fits compared to ModSecurity, the other popular open-source WAF for NGINX:
| Feature | NAXSI | ModSecurity |
|---|---|---|
| Approach | Whitelist (allow known-good) | Blacklist (block known-bad) |
| Core rules | ~95 simple patterns | 100,000+ OWASP CRS signatures |
| Zero-day protection | Strong (blocks unknown patterns) | Weak until signature update |
| Performance impact | Minimal | Moderate to high |
| False positives | Higher initially (requires tuning) | Lower out-of-box |
| Learning mode | Built-in auto-learning | Not available |
| Configuration | Simple, readable syntax | Complex SecLang rules |
| Memory usage | Very low | Higher (large rule sets) |
Choose NAXSI when you want a lightweight WAF with strong zero-day protection. It does require time investment in the initial learning phase. Choose ModSecurity when you need broad out-of-box coverage with minimal tuning.
How the NAXSI WAF Works
Understanding NAXSI internals helps you configure it effectively.
The Scoring System
When a request arrives, NAXSI inspects four zones:
- URL β the request path
- ARGS β query string parameters (GET data)
- BODY β POST data, JSON payloads, and multipart form data
- HEADERS β HTTP headers, including cookies
Each zone is checked against the core rules. When a rule matches, it does not immediately block the request. Instead, it adds points to one or more score categories:
$SQLβ SQL injection indicators$XSSβ cross-site scripting indicators$RFIβ remote file inclusion indicators$TRAVERSALβ directory traversal indicators$EVADEβ encoding evasion tricks$UPLOADβ dangerous file upload indicators
After all rules have been evaluated, NAXSI compares the scores against your thresholds (defined with CheckRule). Only when a threshold is exceeded does NAXSI block the request.
For example, a single apostrophe ' in a query parameter scores $SQL:4 and $XSS:8. A semicolon ; adds another $SQL:4 and $XSS:8. With a threshold of $SQL >= 8, the apostrophe alone would not trigger a block. However, the apostrophe combined with a semicolon pushes SQL to 8 and triggers the block.
Request Processing Flow
Here is how NAXSI processes each request:
- The request enters the NGINX rewrite phase (very early)
- NAXSI checks if the module is enabled for this location
- For POST/PUT requests, NAXSI waits for the request body
- All four zones are scanned against the loaded rules
- Matched rules accumulate scores by category
- Whitelists suppress scoring for specific rule/zone combinations
CheckRulethresholds determine the final action- Blocked requests redirect to the
DeniedUrllocation
Installation
Install the NAXSI WAF module from the GetPageSpeed repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-naxsi
Load the module by adding this line at the top of /etc/nginx/nginx.conf, before any other directives:
load_module modules/ngx_http_naxsi_module.so;
Verify the module loads correctly:
nginx -t
You should see:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
The installation also places the core rules file at /usr/share/nginx/naxsi/naxsi_core.rules. This file contains the default detection patterns.
Basic Configuration
A minimal NAXSI WAF setup requires three things. You must load the core rules, enable NAXSI in a location, and define what happens to blocked requests.
Step 1: Load Core Rules
Add the core rules include inside the http {} block. This must appear before any server {} blocks that use NAXSI:
http {
# Load NAXSI core rules (must be at http level)
include /usr/share/nginx/naxsi/naxsi_core.rules;
# ... your server blocks ...
}
A practical approach is to create a file that loads first alphabetically:
# /etc/nginx/conf.d/00-naxsi-core.conf
include /usr/share/nginx/naxsi/naxsi_core.rules;
Step 2: Enable NAXSI in a Location
Add the NAXSI directives inside the location blocks you want to protect:
server {
listen 80;
server_name example.com;
location / {
# Enable NAXSI WAF
SecRulesEnabled;
# Where to redirect blocked requests
DeniedUrl "/RequestDenied";
# Score thresholds β block when exceeded
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
CheckRule "$UPLOAD >= 8" BLOCK;
proxy_pass http://backend;
}
# Handler for blocked requests
location /RequestDenied {
internal;
return 403 "Blocked by NAXSI WAF";
}
}
The DeniedUrl directive is required when NAXSI is enabled. Without it, NGINX refuses to start with: Missing DeniedURL, abort.
Step 3: Test the Configuration
Test that NGINX accepts the configuration:
nginx -t
Then reload NGINX:
systemctl reload nginx
Verify NAXSI blocks a SQL injection attempt:
curl -o /dev/null -w "%{http_code}" "http://localhost/?id=1 UNION SELECT * FROM users"
You should see 403 β the request was blocked. A legitimate request returns 200:
curl -o /dev/null -w "%{http_code}" "http://localhost/"
Understanding the Core Rules
The core rules file (/usr/share/nginx/naxsi/naxsi_core.rules) contains about 95 rules organized by attack category. Here is what each category detects.
SQL Injection (IDs 1000β1099)
These rules detect SQL keywords, special characters, and hex encoding. For example:
MainRule "rx:select|union|update|delete|insert|table|from|ascii|hex|unhex|drop"
"msg:sql keywords" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie"
"s:$SQL:4" id:1000;
This rule uses a regular expression (rx:) to match SQL keywords. It checks the request body, URL, query arguments, and cookies. Each match adds 4 points to the $SQL score.
Cross-Site Scripting (IDs 1300β1399)
XSS rules detect HTML tags (<, >), JavaScript syntax (`, [, ]), and double-encoding. The angle brackets score $XSS:8 each. A single <script> tag immediately exceeds a threshold of 8.
Directory Traversal (IDs 1200β1299)
These rules catch path traversal sequences like .., /etc/passwd, and c:\\. They also detect encoded bypass variants like /.%2e/ and /..;/.
Remote File Inclusion (IDs 1100β1199)
RFI rules detect URL scheme injections: http://`,https://`, ftp://`,php://,file://,phar://,data://`, and other dangerous protocol handlers.
File Upload (IDs 1500β1600)
Upload rules check filenames in multipart form data. They flag dangerous extensions (.php, .asp, .jsp, .ht) and non-printable ASCII characters.
Learning Mode: Training NAXSI for Your Application
The most important NAXSI WAF feature is learning mode. When enabled, NAXSI logs violations but does not block requests. This lets you observe false positives and build whitelists.
Enable Learning Mode
Add the LearningMode directive to your location:
location / {
SecRulesEnabled;
LearningMode;
DeniedUrl "/RequestDenied";
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
CheckRule "$UPLOAD >= 8" BLOCK;
proxy_pass http://backend;
}
NAXSI still evaluates all rules and logs violations. However, it lets every request through. Log entries show config=learning instead of config=block:
NAXSI_FMT: ip=192.168.1.50&server=example.com&uri=/search&config=learning
&rid=6c0da6921b75285443d1f0e4bbf625d3&cscore0=$SQL&score0=12
&zone0=ARGS&id0=1000&var_name0=q
This log entry tells you:
- ip: the client IP address
- uri: the request path (
/search) - id0: rule 1000 (SQL keywords) matched
- zone0: the match was in ARGS (query string)
- var_name0: specifically the parameter named
q - score0: the SQL score reached 12
Analyze Learning Logs
Run your application normally while learning mode is active. Browse pages, submit forms, use search, and upload files. Then analyze the NAXSI log entries:
grep "NAXSI_FMT" /var/log/nginx/error.log | grep "config=learning"
For each false positive, create a whitelist rule. For example, if the search parameter q legitimately contains SQL-like words:
BasicRule wl:1000 "mz:$ARGS_VAR:q";
This tells NAXSI: βSkip rule 1000 when checking the parameter named q.β
Recommended Learning Workflow
- Deploy with LearningMode on a staging environment
- Exercise the application β every form, endpoint, and workflow
- Collect logs for at least 24β48 hours
- Generate whitelists from the log analysis
- Test whitelists with learning mode still on
- Remove LearningMode to switch to active blocking
- Monitor logs closely for the first few days
Whitelisting Rules
Whitelists are the core of NAXSI WAF tuning. They tell NAXSI which matches to ignore for specific parameters, URLs, or zones.
Whitelist Syntax
The basic syntax is:
BasicRule wl:<rule_ids> "mz:<match_zone>";
wl:β comma-separated rule IDs to whitelist (usewl:0for all rules)mz:β the match zone where the whitelist applies
Whitelist by Parameter Name
Whitelist specific rules for a named parameter:
# Allow SQL keywords in the "query" GET parameter
BasicRule wl:1000 "mz:$ARGS_VAR:query";
# Allow double quotes and commas in the "data" POST parameter
BasicRule wl:1001,1015 "mz:$BODY_VAR:data";
# Allow HTML tags in "content" POST parameter (rich text editors)
BasicRule wl:1302,1303 "mz:$BODY_VAR:content";
# Allow special characters in the Cookie header
BasicRule wl:1001,1015 "mz:$HEADERS_VAR:Cookie";
Whitelist by URL
Apply whitelists only for specific URLs:
# Allow SQL keywords in ARGS for the /api/search endpoint
BasicRule wl:1000 "mz:$URL:/api/search|ARGS";
# Allow all rules for POST body on the /webhook endpoint
BasicRule wl:0 "mz:$URL:/webhook|BODY";
Whitelist with Regex Patterns
Use regex match zones for broader patterns:
# Allow SQL keywords in ARGS for URLs starting with /api/
BasicRule wl:1000 "mz:$URL_X:^/api/|ARGS";
# Allow special chars in POST parameters starting with "json_"
BasicRule wl:1001,1010,1011,1015 "mz:$BODY_VAR_X:^json_";
Whitelist by Zone
Apply whitelists broadly across an entire zone:
# Allow double quotes anywhere in the request body
BasicRule wl:1001 "mz:BODY";
# Allow all rules for entire ARGS zone (not recommended)
BasicRule wl:0 "mz:ARGS";
WordPress Whitelists
Running WordPress behind NAXSI requires whitelists for common operations. Here is a practical starting set:
# WordPress admin AJAX
BasicRule wl:1000,1001,1005,1008,1009,1010,1011,1013,1015,1302,1303,1310,1311,1315 "mz:$URL:/wp-admin/admin-ajax.php|BODY";
# WordPress post editor (Gutenberg sends JSON with HTML)
BasicRule wl:1001,1005,1008,1010,1011,1013,1015,1302,1303,1310,1311,1314,1315 "mz:$URL_X:^/wp-json/|BODY";
BasicRule wl:1001,1005,1008,1010,1011,1013,1015,1302,1303,1310,1311,1314,1315 "mz:$URL_X:^/wp-json/|ARGS";
# WordPress login
BasicRule wl:1001,1009,1010,1011,1013 "mz:$URL:/wp-login.php|BODY";
# WordPress search
BasicRule wl:1000,1008,1009,1010,1011,1302,1303 "mz:$ARGS_VAR:s";
Test these with your specific setup. Plugins and themes may need more whitelists.
Advanced Features
Libinjection Integration
NAXSI integrates libinjection for advanced SQL injection and XSS detection. The core rules use simple pattern matching. Libinjection uses tokenization, which is harder to evade.
Enable libinjection in your location block:
location / {
SecRulesEnabled;
LibInjectionSql;
LibInjectionXss;
DeniedUrl "/RequestDenied";
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
CheckRule "$LIBINJECTION_SQL >= 8" BLOCK;
CheckRule "$LIBINJECTION_XSS >= 8" BLOCK;
proxy_pass http://backend;
}
Libinjection matches use internal rule IDs 17 (SQL) and 18 (XSS). They score against $LIBINJECTION_SQL and $LIBINJECTION_XSS respectively.
IP-Based Bypass
Exempt trusted IP addresses from all NAXSI checks:
location / {
SecRulesEnabled;
# Bypass for monitoring and trusted networks
IgnoreIP "10.0.0.50";
IgnoreCIDR "192.168.1.0/24";
DeniedUrl "/RequestDenied";
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
proxy_pass http://backend;
}
Requests from whitelisted IPs skip all NAXSI rule processing.
JSON Logging
For easier log parsing and SIEM integration, enable JSON-formatted logs:
server {
set $naxsi_json_log 1;
location / {
SecRulesEnabled;
DeniedUrl "/RequestDenied";
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
proxy_pass http://backend;
}
}
JSON logs work well with jq, Elasticsearch, and Splunk:
{"ip":"192.168.1.50","server":"example.com","uri":"/","config":"block",
"rid":"51f332855b2f19204295b320f961e1bc","cscore0":"$SQL","score0":"12",
"zone0":"ARGS","id0":"1000","var_name0":"id"}
Runtime Variable Overrides
NAXSI supports runtime variables for per-request control. Use NGINX set or map directives:
| Variable | Purpose |
|---|---|
$naxsi_flag_enable |
Enable/disable NAXSI per-request |
$naxsi_flag_learning |
Enable learning mode per-request |
$naxsi_extensive_log |
Enable verbose logging |
$naxsi_json_log |
Enable JSON log format |
$naxsi_flag_libinjection_sql |
Enable libinjection SQL per-request |
$naxsi_flag_libinjection_xss |
Enable libinjection XSS per-request |
For example, enable learning mode only for internal networks:
geo $naxsi_flag_learning {
default 0;
192.168.0.0/16 1; # Learning mode for internal network
10.0.0.0/8 1; # Learning mode for VPN
}
server {
location / {
SecRulesEnabled;
DeniedUrl "/RequestDenied";
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
proxy_pass http://backend;
}
}
NAXSI Variables for Custom Logging
NAXSI exports variables for use in log_format or backend headers:
| Variable | Description |
|---|---|
$naxsi_block |
1 if blocked, 0 otherwise |
$naxsi_score |
Current cumulative score |
$naxsi_match |
Matched rule details |
$naxsi_attack_family |
Attack category (SQL, XSS, etc.) |
$naxsi_attack_action |
Action taken (BLOCK, LOG, ALLOW) |
$naxsi_request_id |
Unique 32-character hex identifier |
$naxsi_server |
Server name from request |
$naxsi_uri |
URL-encoded request URI |
$naxsi_learning |
1 if learning mode, 0 otherwise |
$naxsi_total_processed |
Total requests processed |
$naxsi_total_blocked |
Total requests blocked |
Use these in a custom log format for WAF monitoring:
log_format waf_log '$remote_addr - $request '
'naxsi_block=$naxsi_block '
'naxsi_score=$naxsi_score '
'naxsi_attack=$naxsi_attack_family '
'naxsi_rid=$naxsi_request_id';
server {
access_log /var/log/nginx/waf.log waf_log;
# ...
}
Disabling NAXSI for Specific Locations
Some locations should not use WAF protection. Health checks are a good example. Use SecRulesDisabled:
location /health {
SecRulesDisabled;
return 200 "OK";
}
Alternatively, disable via a runtime variable:
location /internal-api {
set $naxsi_flag_enable 0;
proxy_pass http://internal-backend;
}
Fail2ban Integration
Combine the NAXSI WAF with Fail2ban to ban persistent attackers. NAXSI blocks individual malicious requests. Fail2ban bans IPs at the firewall level after repeated violations.
Create a Fail2ban filter at /etc/fail2ban/filter.d/naxsi.conf:
[Definition]
failregex = NAXSI_FMT: ip=<HOST>&server=.*&config=block
ignoreregex =
Add a jail in /etc/fail2ban/jail.local:
[naxsi]
enabled = true
port = http,https
filter = naxsi
logpath = /var/log/nginx/error.log
maxretry = 6
findtime = 300
bantime = 3600
This bans any IP that triggers 6 blocks within 5 minutes for 1 hour.
Performance Considerations
NAXSI is designed for minimal performance overhead:
- No rule updates needed β the core rules rarely change
- Simple pattern matching β most rules use substring search (
str:) - Small memory footprint β only ~95 rules loaded into memory
- Early processing β runs in the rewrite phase
- No disk I/O β everything happens in memory
NAXSI typically adds less than 1 ms of latency per request. ModSecurity with the full OWASP CRS can add 5β10 ms due to its larger rule set.
For maximum performance:
- Use
str:rules overrx:rules in custom rules - Keep whitelist rules specific to reduce zone scanning
- Use
IgnoreIPfor monitoring and health check systems
Troubleshooting
NGINX Fails to Start: βMissing DeniedURL, abortβ
This error means SecRulesEnabled is set but DeniedUrl is missing. Every location with NAXSI enabled must have DeniedUrl.
Legitimate Requests Are Blocked
Check the NAXSI logs to find which rules triggered:
grep "NAXSI_FMT" /var/log/nginx/error.log | tail -20
Each log line shows the rule ID (id0), zone (zone0), and parameter name (var_name0). Create targeted whitelist rules from this data.
Everything Is Blocked After Enabling NAXSI
Start with learning mode and analyze traffic before setting thresholds. Common starting values:
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
If too aggressive, increase the values (e.g., $SQL >= 16).
NAXSI Rules Not Applied
Verify these four things:
- Module is loaded (
load_module modules/ngx_http_naxsi_module.so;) - Core rules included at the
http {}level SecRulesEnabledpresent in the location block- NGINX reloaded after changes (
systemctl reload nginx)
Debugging with Extensive Logging
Enable verbose logging for detailed rule evaluation:
set $naxsi_extensive_log 1;
This produces NAXSI_EXLOG entries showing every rule check, not just threshold-exceeding matches.
Complete Directive Reference
| Directive | Context | Description |
|---|---|---|
MainRule |
http |
Define global rules (core rules file) |
BasicRule |
http, server, location |
Location-specific rules or whitelists |
CheckRule |
http, server, location |
Score thresholds and actions |
DeniedUrl |
http, server, location |
Blocked request handler URL |
SecRulesEnabled |
http, server, location |
Enable NAXSI |
SecRulesDisabled |
http, server, location |
Disable NAXSI |
LearningMode |
http, server, location |
Enable log-only mode |
LibInjectionSql |
http, server, location |
Libinjection SQL detection |
LibInjectionXss |
http, server, location |
Libinjection XSS detection |
IgnoreIP |
http, server, location |
Bypass NAXSI for an IP |
IgnoreCIDR |
http, server, location |
Bypass NAXSI for CIDR range |
NaxsiLogFile |
http, server, location |
Custom NAXSI log file |
All directives support lowercase underscore names too: main_rule, basic_rule, check_rule, denied_url, rules_enabled, rules_disabled, learning_mode, libinjection_sql, libinjection_xss, ignore_ip, ignore_cidr.
Conclusion
The NAXSI WAF provides a fundamentally different approach to web application security. Its whitelist-based architecture catches unknown attacks that signature-based WAFs miss. The learning mode makes initial deployment practical for complex applications. And the minimal performance overhead makes it suitable for high-traffic servers.
For most NGINX deployments on Rocky Linux and RHEL, NAXSI is a strong choice. Combine it with security headers and Fail2ban for defense in depth.
The NAXSI WAF module is maintained and available through the GetPageSpeed repository. The source code is on GitHub.

