Introduction

As a part of setting up this personal blog I installed NGINX to serve the page itself. I thought I’d try out two of the more commenly used open-source Web Application Firewalls (WAFs) that integrate well with NGINX. These are NAXSI (Nginx Anti XSS & SQL Injection)1 and ModSecurity2.

For now this site is pretty static, so there might not be too much to inject or exploit yet. However there should be some other common web attacks that both these solutions should be able to catch. Since my background is in log management, I wanted to take the opportunity to grab some sample logs of such security alerts as well.

The following configuration was made on an Ubuntu 20.04 machine where NGINX (v1.18.0) was installed through apt.

NAXSI

Typically one would compile NGINX alongside NAXSI from source. I already had NGINX running so it seemed that I had to compile the module as a dynamic extension for NGINX instead. The instructions for both of these options are pretty nicely documented by NGINX3, NAXSI itself4 or for example by DigitalOcean5.

Setting this up on CentOS/RHEL is very similar to these compilation methods.6

However NAXSI has been releasing deb packages since v1.1, so this process had some steps fewer than I had originally anticipated:

  1. Go to NAXSI’s releases page on Github.

  2. Find the latest stable version.

  3. Download & install.

    # Download the deb file. In my case v1.3 for Ubuntu 20.04
    wget https://github.com/nbs-system/naxsi/releases/download/1.3/ubuntu-focal-libnginx-mod-http-naxsi_1.3_amd64.deb -O /tmp/naxsi.deb
    # Install and resolve dependencies just in case.
    sudo dpkg -i /tmp/naxsi.deb
    sudo apt-get install -f
    # The NAXSI module should now be enabled for NGINX 
    less /etc/nginx/modules-enabled/50-mod-http-naxsi.conf

  4. Configure NGINX. In the following example I include all core rules, define a blocked request landing page7, as well as include all community rulesets.
    One can also just manually add some of these stanzas directly into your NGINX configuration. These includes do need to be added in the right place though (http vs. server vs. location blocks). This allows for granluar application of the rules to different sections of your webservice (i.e. only apply SQL rules to sections where there’s sql to-be-injected).

    http {
      # Include core ruleset
      include /usr/share/naxsi/naxsi_core.rules;
      ...
    }
    An example rule from the core rule set that adds a SQL score of 2 to a matching request if a possible suspicious hex encoding is detected in the body, URL, any arguments or cookie.
    MainRule "str:0x" "msg:0x, possible hex encoding" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:2" id:1002;
    The following includes a NGINX location statement which is where NAXSI will redirect users who submit requests that are blocked. Typically you’d want to customize this by adding a custom denied landing page or just redirect back to the root of your site.
    server {
      # Include redirection page after NAXSI blocks. 
      # By default it returns a 418 - "I'm a teapot".
      include /usr/share/naxsi/naxsi_denied_url.conf;
      ...
      # Or customize as follows
      location /NaxsiRequestDenied {
        # Insert denied page / logic here
      }
    }
    In the next section I put NAXSI into learning mode, allowing for log analysis to see if potentially legitimate traffic would have been blocked. Include naxsi_block_mode.conf if you want to block requests instead.
    location /  {
      # Set NAXSI to learning mode
      include /usr/share/naxsi/naxsi_learning_mode.conf;
      # Include NAXSI BasicRules
      include /usr/share/naxsi/rules/iris.rules;
      include /usr/share/naxsi/rules/rutorrent.rules;
      include /usr/share/naxsi/rules/wordpress.rules;
      include /usr/share/naxsi/rules/dokuwiki.rules;
      include /usr/share/naxsi/rules/drupal.rules;
      include /usr/share/naxsi/rules/etherpad-lite.rules;
      include /usr/share/naxsi/rules/zerobin.rules;
      ...
    }
    Opening up the naxsi_block_mode.conf and naxsi_learning_mode.conf shows the tunable blocking behavior. A risk score is calculated based on rule types matching the incoming request.

    The main types and their rule IDs are: SQL Injection (1000-1099), Remote File Inclusion (1100-1199), Directory Traversal (1200-1299), Cross Site Scripting (1300-1399), Evading tricks (1400-1500), File Uploads (1500-1600).
    # Action to take when the $SQL score is greater than or equal to 8
    CheckRule "$SQL >= 8" BLOCK;
    CheckRule "$RFI >= 8" BLOCK;
    CheckRule "$TRAVERSAL >= 5" BLOCK;
    CheckRule "$UPLOAD >= 5" BLOCK;
    CheckRule "$XSS >= 8" BLOCK;

  5. Restart NGINX and begin observing your logs to see if any legitimate traffic is getting blocked.

    sudo systemctl restart nginx
    sudo systemctl status nginx
    zgrep "NAXSI" /var/log/nginx/error.log* | less

  6. Start whitelisting rules. This can be done based on IP addresses, URL endpoints, HTTP Methods, File Extensions, regular expressions or all sorts of combinations. A good walkthrough start-to-finish can also be found here and premade fail-2-ban integration can be found here in case you want to start actively blocking incoming connections.

    # Disable naxsi if client ip is 127.0.0.1 in your NGINX configurations.
    if ($remote_addr = "127.0.0.1") {
     set $naxsi_flag_enable 0;
    }
    
    # Disable triggering rule #1000 for GET parameter 'foo' (whitelist rule) 
    BasicRule wl:1000 "mz:$ARGS_VAR:foo";


Modsecurity

Modsecurity is a WAF originally written for the Apache Webserver and has since been ported to Microsoft’s IIS and NGINX. It seems to have a more traditional approach to rulesets, blocking known bad attacks. The most popular and most widely supported collection of open source rules for Modsecurity is called CRS (Core Rule Set) which is published and maintained by OWASP.8 9

It seems that at least two of the largest cloud providers (GCP / Azure) are using Modsecurity with CRS as a WAF as a service.

Modsecurity CRS in Azure Azure Custom Rule Creation
  1. Install libmodsecurity.10
    This is the core library that contains all rules and functionality, later this will be linked against NGINX.11 Go to github releases, find a stable version (v3.0.4 in this case), verify and install. Alternatively one can go through this process using a git clone of the repository and skip to the dependency installation step.11

    # Download source and shasum
    wget https://github.com/SpiderLabs/ModSecurity/releases/download/v3.0.4/modsecurity-v3.0.4.tar.gz
    wget https://github.com/SpiderLabs/ModSecurity/releases/download/v3.0.4/modsecurity-v3.0.4.tar.gz.sha256
    
    # Verify download
    cat modsecurity-v3.0.4.tar.gz.sha256 
    b4231177dd80b4e076b228e57d498670113b69d445bab86db25f65346c24db22  modsecurity-v3.0.4.tar.gz
    sha256sum modsecurity-v3.0.4.tar.gz
    b4231177dd80b4e076b228e57d498670113b69d445bab86db25f65346c24db22  modsecurity-v3.0.4.tar.gz
    sha256sum -c modsecurity-v3.0.4.tar.gz.sha256
    modsecurity-v3.0.4.tar.gz: OK
    
    # Untar/compress
    tar -xzf modsecurity-v3.0.4.tar.gz
    cd modsecurity-v3.0.4/
    
    # Install dependencies
    apt-get install -y apt-utils autoconf automake build-essential git \
                       libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre++-dev \
                       libtool libxml2-dev libyajl-dev pkgconf wget zlib1g-dev
    
    # Build and install
    ./build.sh
    ./configure
    make
    sudo make install

  2. Setup the NGINX connector for modsecurity.11
    In the previous step we setup modsecurity, but NGINX still needs to be able to make use of it. This step will download the source code for the NGINX - modsecurity connector and compile it as a dynamic module.12 13

    # Download the connector source code from GitHub
    git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
    
    # Determine your NGINX version
    nginx -v
    nginx version: nginx/1.18.0 (Ubuntu)
    
    # Download NGINX source code corresponding to your running version
    wget http://nginx.org/download/nginx-1.18.0.tar.gz
    tar zxvf nginx-1.18.0.tar.gz
    
    # Compile the NGINX module, referencing the connector source code 
    cd nginx-1.18.0
    ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
    make modules
    
    # Copy the resulting module file to required NGINX location 
    # (NB! This can also be located in /etc/nginx or /usr/lib/nginx/modules/)
    cp objs/ngx_http_modsecurity_module.so /usr/share/nginx/modules/

  3. Load the module in NGINX.

    # Add the load_module directive to the top /etc/nginx/nginx.conf context to enable Modsecurity
    load_module modules/ngx_http_modsecurity_module.so;
    
    # Restart NGINX to verify
    sudo systemctl restart nginx
    sudo systemctl status nginx
    
    # Error if the module is placed in the wrong directory. 
    Feb 19 18:40:43 blog nginx[175527]: nginx: [emerg] dlopen() "/usr/share/nginx/modules/ngx_http_modsecurity_module.so" failed (/usr/share/nginx/modules/ngx_http_modsecurity_module.so: cannot open shared object file: No such file or directory) in /etc/nginx/nginx.conf:7

  4. Configure NGINX.
    Setup default Modsecurity configuration file and enable the OWASP CRS.

    # Create directory structure and download default conf file
    mkdir /etc/nginx/modsec
    wget -P /etc/nginx/modsec/ https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended
    mv /etc/nginx/modsec/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
    
    # Copy essential file for Modsecurity to newly created directory.
    # This file is part of the original Modsecurity source code, not the module.
    cp modsecurity-v3.0.4/unicode.mapping /etc/nginx/modsec/
    
    # Download latest CRS from https://github.com/SpiderLabs/owasp-modsecurity-crs/releases
    wget https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/v3.2.0.tar.gz
    tar -xzvf v3.2.0.tar.gz
    sudo mv owasp-modsecurity-crs-3.2.0 /usr/local
    
    # Create crs-setup.conf file
    cd /usr/local/owasp-modsecurity-crs-3.2.0
    sudo cp crs-setup.conf.example crs-setup.conf
    
    # Create Modsecurity main.conf file
    touch /etc/nginx/modsec/main.conf
    Copy the collapsed content (press arrow) in the /etc/nginx/modsec/main.conf file. This includes the default configuration file created earlier as well as most CRS rules.

    # Include the recommended configuration
    Include /etc/nginx/modsec/modsecurity.conf
    # OWASP CRS v3 rules
    Include /usr/local/owasp-modsecurity-crs-3.2.0/crs-setup.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-901-INITIALIZATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-910-IP-REPUTATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-912-DOS-PROTECTION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-913-SCANNER-DETECTION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-921-PROTOCOL-ATTACK.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-949-BLOCKING-EVALUATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-950-DATA-LEAKAGES.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-980-CORRELATION.conf
    Include /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf


    NB! These rule files could change and more could be added in later versions, so checkout the /usr/local directory for additional rulesets. Some rulesets are very specific to your environment so you should probably enable / disable these depending on your needs. E.g. are you running NEXTCLOUD, Drupal, PHP or NodeJS?

    Then enable active blocking of traffic, prepare the relevant server blocks so that the modsecurity engine is enabled and you point to it’s configuration files. Also create some whitelist rule-files in case you want to exclude some specfic parts of your site from being examined.
    # Enable blocking of traffic by sed, or manually edit your modsecurity.conf file 
    sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf
    
    # Enable Modsecurity rules for your needed server block. 
    # Add the follwing to /etc/nginx/sites-available/something.conf
    server {
        # ...
        modsecurity on;
        modsecurity_rules_file /etc/nginx/modsec/main.conf;
    }
    
    # Create a symlink for your block
    sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/your_domain
    
    # Create whitelists for your local site here
    touch /usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
    touch /usr/local/owasp-modsecurity-crs-3.2.0/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
    
    # Restart NGINX, enabling the rules
    sudo systemctl restart nginx

  5. Change default log format / options (Optional).14
    In case you installed libyajl-dev as described in step 1, you can change the default logging format from multi-line to JSON. A lot of information can be part of the logs including things like the entire request/response bodies. This information will be logged over multiple lines, so JSON based logs can make things a little bit more readable on the CLI (e.g. by piping to jq). Changing this format to JSON will also help with log aggregation and downstream log processing. This parameter van be set in /etc/nginx/modsec/modsecurity.conf.

    # Change logging format to JSON from default Native
    SecAuditLogFormat JSON
    A second parameter to look at is the SecAuditLogType. By default this is set to “Serial” which means all audit logs are written to a single file. This approach does not scale well, so a “Concurrent” setting exists which creates a file for each incoming request, allowing for parallel writes. This setting is also needed if you want/need to make use of Modsecurity’s native log collector/forwarder, mlogc.
    # Default
    SecAuditLogType Serial
    # Concurrent
    SecAuditLogType Concurrent


Log Samples

The following are some example logs generated on the webserver this blog is running on.

  1. NAXSI
    /var/log/nginx/error.log:2021/01/31 12:24:23 [error] 60362#60362: *800 NAXSI_FMT: ip=206.189.101.16&server=frank-korving.com&uri=/&vers=1.3&total_processed=277&total_blocked=37&config=learning&cscore0=$LIBINJECTION_XSS&score0=8&cscore1=$SQL&score1=16&cscore2=$XSS&score2=64&zone0=ARGS&id0=18&var_name0=q&zone1=ARGS&id1=1001&var_name1=q&zone2=ARGS&id2=1010&var_name2=q&zone3=ARGS&id3=1011&var_name3=q&zone4=ARGS&id4=1302&var_name4=q&zone5=ARGS&id5=1303&var_name5=q, client: 206.189.101.16, server: frank-korving.com, request: "GET /?q="><script>alert(1)</script> HTTP/2.0", host: "frank-korving.com"
    /var/log/nginx/error.log.1:2021/01/30 02:34:35 [error] 60362#60362: *507 NAXSI_FMT: ip=192.241.220.119&server=206.189.101.16&uri=/owa/auth/logon.aspx&vers=1.3&total_processed=138&total_blocked=18&config=learning&cscore0=$RFI&score0=8&zone0=ARGS&id0=1101&var_name0=url, client: 192.241.220.119, server: frank-korving.com, request: "GET /owa/auth/logon.aspx?url=https%3a%2f%2f1%2fecp%2f HTTP/1.1", host: "206.189.101.16"
    In the below block I’m looking at some of the uri paths that can be seen as requested and are part of suspicious requests.
    $ zgrep NAXSI error.log* | grep -Po "request: \"\w+ \K([^\s]+)" | sort -u
    /
    /?a=fetch&content=<php>die(@md5(HelloThinkCMF))</php>
    /api/jsonws/invoke
    /Autodiscover/Autodiscover.xml
    /dns-query
    /index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=HelloThinkPHP21
    /mifs/.;/services/LogService
    /owa/auth/logon.aspx?url=https%3a%2f%2f1%2fecp%2f
    /?q="><script>alert(1)</script>
    /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
    This shows us bots / automated scans looking for things like:

  1. Modsecurity (JSON)
    {"transaction":{"client_ip":"127.0.0.1","time_stamp":"Sun Feb 21 17:59:32 2021","server_id":"1123a898d1f113e782a1b9e0509c6b76a7bb22fd","client_port":60098,"host_ip":"127.0.0.1","host_port":443,"unique_id":"161393037272.133924","request":{"method":"GET","http_version":2.0,"uri":"/?q=\"><script>alert(1)</script>","headers":{"host":"localhost","user-agent":"curl/7.68.0","accept":"*/*"}},"response":{"body":"<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n        <title>Blog &middot; Frank Korving</title>\n        <meta name=\"description\" content=\"On coding, IT Engineering and Security\">\n        <meta name=\"HandheldFriendly\" content=\"True\">\n        <meta name=\"MobileOptimized\" content=\"320\">\n        <meta name=\"generator\" content=\"Hugo 0.80.0\" />\n        <meta name=\"robots\" content=\"index,follow\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n        \n        <link rel=\"stylesheet\" href=\"https://frank-korving.com/dist/site.css\">\n        <link rel=\"stylesheet\" href=\"https://frank-korving.com/dist/syntax.css\">\n        <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,600,700,300&subset=latin,cyrillic-ext,latin-ext,cyrillic\">\n        <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\" integrity=\"sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN\" crossorigin=\"anonymous\">\n        \n            <link href=\"/index.xml\" rel=\"alternate\" type=\"application/rss+xml\">\n        \n        \n        \n        \n        \n\n    </head>\n    <body>\n        \n\n        <div id=\"wrapper\">\n            <header class=\"site-header\">\n                <div class=\"container\">\n                    <div class=\"site-title-wrapper\">\n                        \n                            <h1 class=\"site-title\">\n                                <a href=\"https://frank-korving.com/\">Blog</a>\n                            </h1>\n                        \n                        \n                            <a class=\"button-square\" href=\"https://frank-korving.com/index.xml\" aria-label=\"RSS\"><i class=\"fa fa-rss\" aria-hidden=\"true\"></i></a>\n                        \n                        \n                            <a class=\"button-square button-social hint--top\" data-hint=\"Twitter\" aria-label=\"Twitter\" href=\"https://twitter.com/Frank_Korving\" rel=\"me\" >\n                                <i class=\"fa fa-twitter\" aria-hidden=\"true\"></i>\n                            </a>\n                        \n                        \n                        \n                        \n                            <a class=\"button-square button-social hint--top\" data-hint=\"Github\" aria-label=\"Github\" href=\"https://github.com/Korving-F\" rel=\"me\">\n                                <i class=\"fa fa-github-alt\" aria-hidden=\"true\"></i>\n                            </a>\n                        \n                        \n                        \n                            <a class=\"button-square button-social hint--top\" data-hint=\"LinkedIn\" aria-label=\"LinkedIn\" href=\"https://www.linkedin.com/in/frank-korving-85494618a/\" rel=\"me\">\n                                <i class=\"fa fa-linkedin\" aria-hidden=\"true\"></i>\n                            </a>\n                        \n                        \n                            <a class=\"button-square button-social hint--top\" data-hint=\"Email\" aria-label=\"Send an Email\" href=\"mailto:korving.f@gmail.com\">\n                                <i class=\"fa fa-envelope\" aria-hidden=\"true\"></i>\n                            </a>\n                        \n                    </div>\n\n                    <ul class=\"site-nav\">\n                        \n    <li class=\"site-nav-item\">\n        <a href=\"/tags/engineering/\">Engineering</a>\n    </li>\n\n    <li class=\"site-nav-item\">\n        <a href=\"/tags/book/\">Books</a>\n    </li>\n\n    <li class=\"site-nav-item\">\n        <a href=\"/tags/coding/\">Coding</a>\n    </li>\n\n    <li class=\"site-nav-item\">\n        <a href=\"/tags/security/\">Security</a>\n    </li>\n\n\n                    </ul>\n                </div>\n            </header>\n\n            <div id=\"container\">\n\n\n<div id=\"post-index\" class=\"container\" itemscope=\"\" itemtype=\"https://schema.org/Blog\">\n    \n    <header class=\"post-header\">\n        <h1 class=\"post-title\" itemprop=\"name\">Frank Korving</h1>\n        <p>On coding, IT Engineering and Security</p>\n    </header>\n\n\n\n    <ol class=\"post-list\">\n        \n        \n            <li class=\"post-stub\" itemprop=\"blogPost\" itemscope=\"\" itemtype=\"https://schema.org/BlogPosting\">\n    <a href=\"https://frank-korving.com/posts/hugo-site-generation/\" itemprop=\"url\">\n        <h4 class=\"post-stub-title\" itemprop=\"name\">Hugo Static Site Generation</h4>\n        <time class=\"post-stub-date\" datetime=\"2021-01-08\">Published Fri, Jan 8, 2021</time>\n        \n    </a>\n</li>\n\n        \n    </ol>\n\n    <div class=\"post-navigation\">\n        \n\n    </div>\n</div>\n\n            </div>\n        </div>\n\n        <footer class=\"footer\">\n            <div class=\"container\">\n                <div class=\"site-title-wrapper\">\n                    <h1 class=\"site-title\">\n                        <a href=\"https://frank-korving.com/\">Blog</a>\n                    </h1>\n                    <a class=\"button-square button-jump-top js-jump-top\" href=\"#\" aria-label=\"Back to Top\">\n                        <i class=\"fa fa-angle-up\" aria-hidden=\"true\"></i>\n                    </a>\n                </div>\n\n                <p class=\"footer-copyright\">\n                    <span>&copy; 2021 / Powered by <a href=\"https://gohugo.io/\">Hugo</a></span>\n                </p>\n                <p class=\"footer-copyright\">\n                    <span><a href=\"https://github.com/roryg/ghostwriter\">Ghostwriter theme</a> By <a href=\"http://jollygoodthemes.com\">JollyGoodThemes</a></span>\n                    <span>/ <a href=\"https://github.com/jbub/ghostwriter\">Ported</a> to Hugo By <a href=\"https://github.com/jbub\">jbub</a></span>\n                </p>\n            </div>\n        </footer>\n\n        <script src=\"https://frank-korving.com/js/jquery-1.11.3.min.js\"></script>\n        <script src=\"https://frank-korving.com/js/jquery.fitvids.js\"></script>\n        <script src=\"https://frank-korving.com/js/scripts.js\"></script>\n    </body>\n</html>\n\n","http_code":200,"headers":{"Server":"nginx/1.18.0","Date":"Sun, 21 Feb 2021 17:59:32 GMT","Content-Length":"5999","Content-Type":"text/html","Last-Modified":"Sat, 23 Jan 2021 13:02:33 GMT","Connection":"close","ETag":"\"600c1e69-176f\"","Strict-Transport-Security":"max-age=63072000"}},"producer":{"modsecurity":"ModSecurity v3.0.4 (Linux)","connector":"ModSecurity-nginx v1.0.1","secrules_engine":"DetectionOnly","components":["OWASP_CRS/3.2.0\""]},"messages":[{"message":"XSS Attack Detected via libinjection","details":{"match":"detected XSS using libinjection.","reference":"v8,27t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls","ruleId":"941100","file":"/usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf","lineNumber":"37","data":"Matched Data: XSS data found within ARGS:q: \"><script>alert(1)</script>","severity":"2","ver":"OWASP_CRS/3.2.0","rev":"","tags":["application-multi","language-multi","platform-multi","attack-xss","OWASP_CRS","OWASP_CRS/WEB_ATTACK/XSS","WASCTC/WASC-8","WASCTC/WASC-22","OWASP_TOP_10/A3","OWASP_AppSensor/IE1","CAPEC-242"],"maturity":"0","accuracy":"0"}},{"message":"XSS Filter - Category 1: Script Tag Vector","details":{"match":"Matched \"Operator `Rx' with parameter `(?i)<script[^>]*>[\\s\\S]*?' against variable `ARGS:q' (Value: `\"><script>alert(1)</script>' )","reference":"o2,8v8,27t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls","ruleId":"941110","file":"/usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf","lineNumber":"67","data":"Matched Data: <script> found within ARGS:q: \"><script>alert(1)</script>","severity":"2","ver":"OWASP_CRS/3.2.0","rev":"","tags":["application-multi","language-multi","platform-multi","attack-xss","OWASP_CRS","OWASP_CRS/WEB_ATTACK/XSS","WASCTC/WASC-8","WASCTC/WASC-22","OWASP_TOP_10/A3","OWASP_AppSensor/IE1","CAPEC-242"],"maturity":"0","accuracy":"0"}},{"message":"NoScript XSS InjectionChecker: HTML Injection","details":{"match":"Matched \"Operator `Rx' with parameter `(?i:(?:<\\w[\\s\\S]*[\\s\\/]|['\\\"](?:[\\s\\S]*[\\s\\/])?)(?:on(?:d(?:e(?:vice(?:(?:orienta|mo)tion|proximity|found|light)|livery(?:success|error)|activate)|r(?:ag(?:e(?:n(?:ter|d)|xit)|(?:gestur|leav)e|start|d (3139 characters omitted)' against variable `ARGS:q' (Value: `\"><script>alert(1)</script>' )","reference":"o2,7o18,8v8,27t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls","ruleId":"941160","file":"/usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf","lineNumber":"195","data":"Matched Data: <script found within ARGS:q: \"><script>alert(1)</script>","severity":"2","ver":"OWASP_CRS/3.2.0","rev":"","tags":["application-multi","language-multi","platform-multi","attack-xss","OWASP_CRS","OWASP_CRS/WEB_ATTACK/XSS","WASCTC/WASC-8","WASCTC/WASC-22","OWASP_TOP_10/A3","OWASP_AppSensor/IE1","CAPEC-242"],"maturity":"0","accuracy":"0"}},{"message":"Inbound Anomaly Score Exceeded (Total Score: 15)","details":{"match":"Matched \"Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `15' )","reference":"","ruleId":"949110","file":"/usr/local/owasp-modsecurity-crs-3.2.0/rules/REQUEST-949-BLOCKING-EVALUATION.conf","lineNumber":"79","data":"","severity":"2","ver":"","rev":"","tags":["application-multi","language-multi","platform-multi","attack-generic"],"maturity":"0","accuracy":"0"}}]}}

Conclusion

NAXSI’s drop-by-default approach seems more likely to cause false positives and potential blocking of legitimate traffic. Modsecurity seems more likely to have some false negatives out-of-the-box since the rules are looking for known bads.

All in all this was a pretty interesting topic to dive into ^_^

Things worth looking into some more:

  1. Performance. There’s likely to be some performance trade-off between the two approaches when working at scale. Especially taking into account complex and varied URI paths, method types, header options etc.

    I tried some basic benchmarking using wrk2 and the following commands which I derived using trial and error, finding the limits of my little DigitalOcean droplet. The following commands output hgrm format which can be nicely visualized using various packages or on the web here.

    I’m sure the following misses some obvious subtleties, however it is clear to see that the out-of-the-box rulesets for modsecurity cause an order of magnitude higher latency. I’m interested to see what the performance impact of a production NAXSI ruleset with appropriate exclusions would be. In addition one could try to see what the licencing/usage model for Modsecurity WAFs are on platforms like GCP / Azure. Is it worth the money / effort to hire some fulltime WAF maintainers which update your company’s own rulebase and balance out the costs of additional latency / CPU cycles on such platforms?

    # No WAF
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com" | tee nothing_no_inject
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com/?a=fetch&content=<php>die(@md5(HelloThinkCMF))</php>" | tee nothing_inject
    # NAXSI
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com" | tee naxsi_no_inject
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com/?a=fetch&content=<php>die(@md5(HelloThinkCMF))</php>" | tee naxsi_inject
    # Modsecurity
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com" | tee modsecurity_no_inject
    wrk -t1 -c25 -d30s -R50 --latency "https://frank-korving.com/?a=fetch&content=<php>die(@md5(HelloThinkCMF))</php>" | tee modsecurity_inject

  2. Effectiveness. These WAFs are very widely deployed and supported but are therefore also the source of the occasional bypass writeup or CVE. I am curious to see an analysis comparing actual production deployments. Some (recent) examples:

  3. Maintainability. The CRS ruleset for Modsecurity seems like a good start, but you’d probably still want to tune / add rules specific for your environment. NAXSI definitely requires a lot of expertise and attention to keep up with all whitelisting rules for your specific application(s). I wonder if people integrate their WAFs in a DevSecOps pipeline to generate (il)legitimate requests and assist in identifying potential false positives/negatives.


  1. https://github.com/nbs-system/naxsi ↩︎

  2. https://github.com/SpiderLabs/ModSecurity ↩︎

  3. https://www.nginx.com/blog/compiling-dynamic-modules-nginx-plus/ ↩︎

  4. https://github.com/nbs-system/naxsi/wiki/naxsi-compile ↩︎

  5. https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-naxsi-on-ubuntu-16-04 ↩︎

  6. https://gist.github.com/netscylla/f9ba476bbd129587c2afaac1ea0c61ee ↩︎

  7. Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0) - https://tools.ietf.org/html/rfc2324 ↩︎

  8. https://github.com/coreruleset/coreruleset ↩︎

  9. https://coreruleset.org/ ↩︎

  10. https://github.com/SpiderLabs/ModSecurity ↩︎

  11. https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/ ↩︎

  12. https://github.com/SpiderLabs/ModSecurity-nginx ↩︎

  13. https://docs.nginx.com/nginx-waf/admin-guide/nginx-plus-modsecurity-waf-owasp-crs ↩︎

  14. https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29#SecAuditLogFormat ↩︎