Nginx by examples: naxsi WAF

Naxsi is an open source WAF module developed by NBS System and released under GPL v3

In the past a nginx-naxsi standard Ubuntu package was available from the official repositories. Unfortunately this package is no longer maintained so we must now rebuild Nginx from source to use Naxsi

System setup #

Let’s start with a fresh Ubuntu 14.04.x system

# this is needed to build nginx
sudo apt-get install libpcre3 libpcre3-dev libssl-dev unzip make -y

cd /tmp

# we download Nginx

# we download the latest Naxsi source code

tar xvzf nginx-1.8.1.tar.gz
cd nginx-1.8.1/

Now we need to build Nginx with the Naxsi waf module we just downloaded

# a standard configure block where we disable 
# some normally unused nginx modules (POP3 / IMAP / SMTP etc)
./configure --conf-path=/etc/nginx/nginx.conf --add-module=../naxsi-master/naxsi_src/ \
 --error-log-path=/var/log/nginx/error.log --http-client-body-temp-path=/var/lib/nginx/body \
 --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-log-path=/var/log/nginx/access.log \
 --http-proxy-temp-path=/var/lib/nginx/proxy --lock-path=/var/lock/nginx.lock \
 --pid-path=/var/run/ --with-http_ssl_module \
 --without-mail_pop3_module --without-mail_smtp_module \
 --without-mail_imap_module --without-http_uwsgi_module \
 --without-http_scgi_module --with-ipv6 --prefix=/usr

# we compile

# we install in /etc/nginx by default
sudo make install 

# we make sure the /var/lib/nginx folder exists
sudo mkdir /var/lib/nginx

In most cases you should get no warnings and everything should work just fine.

As we are building Nginx from scratch we need to setup a init script manually:

For this exercise we’ll download an use a stock standard sysv startup script borrowed by the official Nginx PPA which I made available on my S3 account.

# download
sudo wget -O /etc/init.d/nginx

# fix the file permissions
sudo chmod a+x /etc/init.d/nginx

# setup on this server
sudo update-rc.d nginx defaults

Naxsi setup #

Naxsi is a WAF) built around a security model which is very strict (almost unusable) by default and needs to be relaxed on a case by case basis. This approach makes the configuration more resilient to future or unknown type of security breaches.

Naxsi works based off a set of strict standard rules available on its Github repository

sudo wget -O /etc/nginx/naxsi_core.rules

These rules change slowly over time as the development of Naxsi continues. Through experience, I’ve setup a list of common relaxation rules that I tend to reuse over and over.

They are just given here as a convenient starting point

# Allows most characters in Cookies
# Without these rules basically no site will ever work
# If your web app doesn't use cookies you can comment them out safely
BasicRule wl:1000 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1001 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1005 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1007 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1010 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1011 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1013 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1015 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1100 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1101 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1314 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1315 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1306 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1310 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1311 "mz:$HEADERS_VAR:Cookie";
BasicRule wl:1401 "mz:$HEADERS_VAR:Cookie";

#  allows " in args
BasicRule wl:1001 "mz:ARGS";

#  allows ' in args
BasicRule wl:1013 "mz:ARGS";

# Allows -- in a URL
BasicRule wl:1007 "mz:URL";

# Allows ; in a URL - not great but used by some CMSs
BasicRule wl:1008 "mz:URL";

# Allows () in a URL
BasicRule wl:1010 "mz:URL";      
BasicRule wl:1011 "mz:URL";

# allows [ and ] in the URL arguments
BasicRule wl:1310 "mz:ARGS";
BasicRule wl:1311 "mz:ARGS";

Once again I’ve made them available online on my S3 account

# download the modified relaxation rules
sudo wget -O /etc/nginx/naxsi_relax.rules

Nginx base config #

For the sake of testing Naxsi out let’s setup our server as a reverse proxy in front of

user www-data;
worker_processes 1;
pid /run/;

events {
  worker_connections 1024;
  # multi_accept on;

http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  server_tokens off;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  # Logging Settings
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  # Gzip Settings
  gzip off;
  gzip_disable "msie6";

  # gzip_vary on;
  # gzip_proxied any;
  gzip_comp_level 2;
  # gzip_buffers 16 8k;
  # gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

  # we include the nginx-naxsi core rules
  include /etc/nginx/naxsi_core.rules;

  # we allow for uploads greater than 1mb (the default)
  client_max_body_size 128m;

  # vhosts
  server {


    access_log /var/log/nginx/;
    error_log /var/log/nginx/ error;


    gzip on;

    location / {  
      proxy_set_header Host $server_name;
      proxy_pass http://$server_name;

      proxy_http_version 1.1;

      ## proxy buffer (up from 8k to 32k)
      proxy_buffer_size   32k;
      proxy_buffers   4 32k;
      proxy_busy_buffers_size   32k;

      ## Naxsi rules
      DeniedUrl "/RequestDenied";

      ## check rules
      CheckRule "$SQL >= 10" BLOCK;
      CheckRule "$RFI >= 8" BLOCK;
      CheckRule "$TRAVERSAL >= 4" BLOCK;
      CheckRule "$EVADE >= 4" BLOCK;
      CheckRule "$XSS >= 8" BLOCK;

      # nginx-naxsi relaxation rules
      include /etc/nginx/naxsi_relax.rules;

    location /RequestDenied {
      return 403;

Naxsi setup #

The following lines govern how Naxsi intercepts and stops potentially malicious requests

# We disable LearningMode

# We enable the rules

# In case of a Denied request we'll redirect to this interal URL 
# (matched by our Nginx config)
DeniedUrl "/RequestDenied";

# All rules come with a weight and are considered in a block
# Here we define the thresholds for each block
CheckRule "$SQL >= 10" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;

# we include the nginx-naxsi relaxation rules discussed before
include /etc/nginx/naxsi_relax.rules;

As we disabled LearningMode (which would have just logged the blocked requests) the WAF is now fully operational

sudo service nginx start

Testing the WAF #

The best way to test our WAF configuration is to verify that normal pages work as expected:

wget --server-response --spider

should yield HTTP code 200

Connecting to connected.
HTTP request sent, awaiting response...
  HTTP/1.1 200 OK
  Server: nginx
  Date: Fri, 11 Mar 2016 00:26:10 GMT
  Content-Type: text/html; charset=utf-8
  Transfer-Encoding: chunked
  Connection: keep-alive
  Vary: Accept-Encoding
  Status: 200 OK

and try to send malicious requests (with PHP code or SQL injection for example) and see them correctly blocked

wget --server-response --spider;%20?%3E

should yield HTTP code 403

Connecting to connected.
HTTP request sent, awaiting response...
  HTTP/1.1 403 Forbidden
  Server: nginx
  Date: Fri, 11 Mar 2016 00:31:56 GMT
  Content-Type: text/html
  Content-Length: 162
  Connection: keep-alive


Now read this

5 reasons for not using db triggers

Triggers are nice little feature that most relational DBMS offer to automatically execute code in response to certain events on a particular table or view. Their primary aim is to maintain data integrity but they are often used to... Continue →