NAXSI (Nginx Anti XSS and SQL Injection) is an open source web application firewall that runs inside Nginx as a module. Instead of a large signature database, it scores each request against a small set of rules that look for characters and keywords used in attacks, and blocks the request when a score passes a threshold. In this tutorial you will compile NAXSI as a dynamic module for the Nginx package that ships with Ubuntu 24.04, enable it in learning mode, whitelist the false positives your application produces, and then switch it to blocking mode.

The original NAXSI project was archived in 2023. Development continues in the wargio/naxsi fork, which is the version used here.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with a non-root user that has sudo privileges.
  • Nginx installed from the Ubuntu repositories (sudo apt install nginx) and a working site, reachable at your_domain. This can be a static site or a reverse proxy to an application.
  • About 500 MB of free disk space for the build.

Step 1 - Installing build dependencies

A dynamic module must be built against the exact Nginx version that will load it. Start by checking the installed version and confirming it was built with --with-compat, which allows modules compiled separately to be loaded:

nginx -v
nginx -V 2>&1 | grep -o with-compat
nginx version: nginx/1.24.0 (Ubuntu)
with-compat

Note the version number; you will download the same Nginx source release in the next step. Now install the compiler and the libraries the build needs:

sudo apt update
sudo apt install build-essential libpcre2-dev zlib1g-dev wget

Step 2 - Compiling the NAXSI module

Work in a build directory in your home folder:

mkdir -p ~/naxsi-build
cd ~/naxsi-build

Download the NAXSI release archive. The src-with-deps archive already includes the libinjection library that NAXSI uses, so you do not need to fetch Git submodules. Check the releases page for the latest version and adjust NAXSI_VERSION if needed:

NAXSI_VERSION=1.7
wget "https://github.com/wargio/naxsi/releases/download/${NAXSI_VERSION}/naxsi-${NAXSI_VERSION}-src-with-deps.tar.gz"
mkdir naxsi
tar -C naxsi -xzf "naxsi-${NAXSI_VERSION}-src-with-deps.tar.gz"

Download the Nginx source for the same version that nginx -v reported:

NGINX_VERSION=1.24.0
wget "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
tar -xzf "nginx-${NGINX_VERSION}.tar.gz"

Configure the source tree in compatibility mode with NAXSI as a dynamic module, then build only the modules:

cd "nginx-${NGINX_VERSION}"
./configure --with-compat --add-dynamic-module=../naxsi/naxsi_src
make modules

During configure you may see No package 'libinjection' found followed by Using submodule libinjection. That is expected: the bundled copy is used. When the build finishes, the module is in objs/:

ls -l objs/ngx_http_naxsi_module.so
-rwxrwxr-x 1 your_user your_user 1203744 Sep 25 10:12 objs/ngx_http_naxsi_module.so

Step 3 - Installing the module and the core rules

Copy the module to the directory where Ubuntu's Nginx keeps its modules:

sudo install -m 0644 objs/ngx_http_naxsi_module.so /usr/lib/nginx/modules/

On Ubuntu, /etc/nginx/nginx.conf includes every file in /etc/nginx/modules-enabled/ at the top of the configuration, which is where load_module directives belong. Create a file for NAXSI:

echo 'load_module modules/ngx_http_naxsi_module.so;' | sudo tee /etc/nginx/modules-enabled/50-mod-http-naxsi.conf

Copy the rules shipped with NAXSI. naxsi_core.rules holds the generic SQL injection, XSS, traversal and remote file inclusion rules; the blocking directory adds rules for scanners, WordPress and PHP probes:

sudo mkdir -p /etc/nginx/naxsi
sudo cp ~/naxsi-build/naxsi/naxsi_rules/naxsi_core.rules /etc/nginx/naxsi/
sudo cp -r ~/naxsi-build/naxsi/naxsi_rules/blocking /etc/nginx/naxsi/

Rules defined with MainRule are global and must be loaded in the http context. Files in /etc/nginx/conf.d/ are included inside the http block on Ubuntu, so create one there:

sudo nano /etc/nginx/conf.d/naxsi.conf
include /etc/nginx/naxsi/naxsi_core.rules;
include /etc/nginx/naxsi/blocking/*.rules;

Test the configuration:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you see module "/usr/lib/nginx/modules/ngx_http_naxsi_module.so" version 1024000 instead of ..., the module was built from a different Nginx version than the one installed. Repeat Step 2 with the correct version.

Step 4 - Enabling NAXSI in learning mode

NAXSI is enabled per location block. Every rule that matches adds points to a named score such as $SQL or $XSS, and CheckRule lines decide what happens when a score reaches a threshold. In learning mode, BLOCK actions are only logged, so you can see what would be blocked without breaking the site.

Open your site's server block:

sudo nano /etc/nginx/sites-available/your_domain

Add the NAXSI directives to the main location and add a /RequestDenied location that returns 403. Keep your existing root, try_files or proxy_pass lines as they are:

server {
    listen 80;
    server_name your_domain;

    # ... existing root, ssl and other directives ...

    location / {
        SecRulesEnabled;
        LearningMode;
        LibInjectionSql;
        LibInjectionXss;
        DeniedUrl "/RequestDenied";

        CheckRule "$SQL >= 8" BLOCK;
        CheckRule "$XSS >= 8" BLOCK;
        CheckRule "$RFI >= 8" BLOCK;
        CheckRule "$UWA >= 8" BLOCK;
        CheckRule "$EVADE >= 8" BLOCK;
        CheckRule "$UPLOAD >= 5" BLOCK;
        CheckRule "$TRAVERSAL >= 5" BLOCK;
        CheckRule "$LIBINJECTION_SQL >= 8" BLOCK;
        CheckRule "$LIBINJECTION_XSS >= 8" BLOCK;

        # ... existing try_files or proxy_pass ...
    }

    location /RequestDenied {
        internal;
        return 403;
    }
}

What the directives do:

  • SecRulesEnabled turns NAXSI on for this location. Without it, no rule is evaluated.
  • LearningMode logs instead of blocking. You will remove it in Step 6.
  • LibInjectionSql and LibInjectionXss add libinjection's parser-based detection, which feeds the $LIBINJECTION_* scores.
  • DeniedUrl is the internal location blocked requests are redirected to.

Every CheckRule score used by the rule files must be present, otherwise those rules never trigger an action. Test and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Verifying detection

Send a request that looks like SQL injection:

curl -s -o /dev/null -w '%{http_code}\n' "http://your_domain/?id=1%22%20union%20select%201"
200

The request succeeds because learning mode is on, but NAXSI logs it to the Nginx error log with the prefix NAXSI_FMT:

sudo grep NAXSI_FMT /var/log/nginx/error.log | tail -n 1
2026/09/25 10:20:41 [error] 4127#4127: *12 NAXSI_FMT: ip=203.0.113.10&server=your_domain&uri=%2F&config=learning&rid=6c0d...&cscore0=$SQL&score0=16&cscore1=$XSS&score1=8&zone0=ARGS&id0=1000&var_name0=id&zone1=ARGS&id1=1001&var_name1=id, client: 203.0.113.10, server: your_domain, request: "GET /?id=1%22%20union%20select%201 HTTP/1.1", host: "your_domain"

The important fields are config=learning (the request would have been blocked), id0, id1 (the rule IDs that matched), zone (where: ARGS, BODY, URL, HEADERS) and var_name (which parameter).

Step 5 - Finding and whitelisting false positives

NAXSI's rules are deliberately broad: a double quote in a search box or HTML in a comment field is enough to trigger them. Leave learning mode on while you use every part of your application normally (log in, submit forms, search, upload files), ideally for a few days of real traffic.

Then list which rules fire most, on which parameter and URL:

sudo grep -h NAXSI_FMT /var/log/nginx/error.log | grep -o 'uri=[^&]*\|zone0=[^&]*\|id0=[^&]*\|var_name0=[^&,]*' | paste - - - - | sort | uniq -c | sort -rn | head
     37 uri=%2Fsearch	zone0=ARGS	id0=1001	var_name0=q
     12 uri=%2Fapi%2Fcomments	zone0=BODY	id0=1302	var_name0=comment
      3 uri=%2F.git%2Fconfig	zone0=URL	id0=20000006	var_name0=

The first two lines are legitimate traffic: users type quotes in the search box and HTML in comments. The third is a scanner looking for an exposed Git repository and should stay blocked.

Whitelists use BasicRule wl:<ids> with a match zone (mz) that limits them to one parameter, URL or both. Add them inside the same location block, below the CheckRule lines:

        # Allow double quotes in the search parameter
        BasicRule wl:1001 "mz:$ARGS_VAR:q";

        # Allow HTML in the comment field, only on the comments endpoint
        BasicRule wl:1302,1303,1310,1311 "mz:$URL:/api/comments|$BODY_VAR:comment";

Keep whitelists as narrow as possible. "mz:$ARGS_VAR:q" only disables rule 1001 for the q parameter; the other rules still apply to it, and rule 1001 still applies everywhere else. Avoid wl:0 (all rules) unless it is limited to a single, trusted URL.

Reload after each change and keep watching the log:

sudo nginx -t && sudo systemctl reload nginx

Step 6 - Switching to blocking mode

When a normal browsing session no longer produces NAXSI_FMT entries for legitimate requests, remove learning mode. In /etc/nginx/sites-available/your_domain, delete or comment out the LearningMode; line:

        # LearningMode;

Reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Repeat the malicious request:

curl -s -o /dev/null -w '%{http_code}\n' "http://your_domain/?id=1%22%20union%20select%201"
403

And confirm that a normal request still works:

curl -s -o /dev/null -w '%{http_code}\n' "http://your_domain/?id=42"
200

The error log entry now shows config=block. Count blocks per rule to keep an eye on new false positives after application changes:

sudo grep -h 'NAXSI_FMT' /var/log/nginx/error.log | grep -o 'config=block.*id0=[0-9]*' | grep -o 'id0=[0-9]*' | sort | uniq -c | sort -rn

Step 7 - Adding a custom rule

You can add your own rules with MainRule (global, in the http context) or BasicRule (inside one location). Rule IDs must be greater than 999 and should not collide with the shipped rules; a high range such as 90000000 is safe. This rule blocks requests for a backup directory that should never be public:

sudo nano /etc/nginx/naxsi/local.rules
MainRule id:90000001 "s:$UWA:8" "str:/backup/" "mz:URL" "msg:access to backup directory";

Strings are case insensitive and NAXSI decodes URL encoding before matching, so /Backup/ and %2Fbackup%2F are caught too. Include the file in /etc/nginx/conf.d/naxsi.conf:

include /etc/nginx/naxsi/naxsi_core.rules;
include /etc/nginx/naxsi/blocking/*.rules;
include /etc/nginx/naxsi/local.rules;

Because the rule scores $UWA and your location already has CheckRule "$UWA >= 8" BLOCK;, the request is blocked. Test it:

sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -w '%{http_code}\n' http://your_domain/backup/db.sql
403

Step 8 - Keeping the module in sync with Nginx updates

When Ubuntu publishes a new Nginx version, the old module no longer matches and nginx -t fails with a version error, so Nginx refuses to reload. To control when this happens, hold the Nginx packages:

sudo apt-mark hold nginx nginx-common

Security updates will then be listed but not applied. When you want to upgrade, rebuild the module for the new version first (Step 2 with the new NGINX_VERSION), then run:

sudo apt-mark unhold nginx nginx-common
sudo apt install --only-upgrade nginx nginx-common
sudo install -m 0644 ~/naxsi-build/nginx-NEW_VERSION/objs/ngx_http_naxsi_module.so /usr/lib/nginx/modules/
sudo nginx -t && sudo systemctl restart nginx
sudo apt-mark hold nginx nginx-common

Replace NEW_VERSION with the new Nginx version number.

Troubleshooting

unknown directive "SecRulesEnabled". The module is not loaded. Check that /etc/nginx/modules-enabled/50-mod-http-naxsi.conf exists and that nginx.conf still contains include /etc/nginx/modules-enabled/*.conf;.

"MainRule" directive is not allowed here. A MainRule line or rule file was included inside a server or location block. Global rules must be included in the http context, as in conf.d/naxsi.conf.

Legitimate POST requests are blocked with rule ID 11 or 2. Internal rule 11 means an unknown Content-Type, and 2 means the body was too large and written to disk. Raise client_body_buffer_size for large forms, or whitelist the rule for that URL with BasicRule wl:11 "mz:$URL:/your/endpoint|BODY";.

NAXSI log lines are cut off. Nginx truncates error log lines at about 2 KB, so requests with many matches lose the last fields. Enable JSON logs with set $naxsi_json_log 1; in the server block for shorter, easier to parse entries.

Conclusion

NAXSI now inspects every request to your site, blocks SQL injection, XSS, traversal and scanner traffic, and allows the specific parameters your application needs. The learning, whitelist and block cycle is the key to running it well: repeat it whenever you deploy features that accept new kinds of input. As next steps, ban repeat offenders at the firewall with a Fail2Ban filter on NAXSI_FMT lines, enable set $naxsi_json_log 1; and ship the logs to your log platform, and put HTTPS in front of the site with Let's Encrypt if you have not already.