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


comments powered by Disqus

Subscribe to my newsletter