Implementing a Web Application Firewall

To protect a web application against security flaws, you can use mod_security, a module for the well-known, serving 50% of the internet, Apache httpd.

What does it protect?

This list is taken from GitHub page of mod-security and shows the possibilities of mod-security:

  • HTTP Protection – detecting violations of the HTTP protocol and a locally defined usage policy.
  • Real-time Blacklist Lookups – utilizes 3rd Party IP Reputation
  • Web-based Malware Detection – identifies malicious web content by check against the Google Safe Browsing API.
  • HTTP Denial of Service Protections – defense against HTTP Flooding and Slow HTTP DoS Attacks.
  • Common Web Attacks Protection – detecting common web application security attack.
  • Automation Detection – Detecting bots, crawlers, scanners and other surface malicious activity.
  • Integration with AV Scanning for File Uploads – detects malicious files uploaded through the web application.
  • Tracking Sensitive Data – Tracks Credit Card usage and blocks leakages.
  • Trojan Protection – Detecting access to Trojans horses.
  • Identification of Application Defects – alerts on application misconfigurations.
  • Error Detection and Hiding – Disguising error messages sent by the server.

Big Picture

         +-------------+
         | Client      |       Port 80 redirect to 443
         |-------------|+---------------------+
         | Browser     |                      |
         +-------------+                      | Port 443 - Terminate SSL
                                              v
                               +------------------------------+
                               | Web Application Firewall     |
                               |------------------------------|
                               | Apache, mod_security,        |
                               | mod_rewrite, mod_proxy       |
                               +------------------------------+
                                     +                 +
                              vhost1 |                 | vhost2
                                     |                 |
                                     |                 |
                             Port 80 |                 | Port 80
                                     v                 v
                               +-------------+  +-------------+
                               | Application |  | Application |
                               |-------------|  |-------------|
                               |             |  |             |
               +----------+    |             |  |             |    +----------+
               | Database |    |  Java       |  |  PHP        |    | Database |
               |----------|    |             |  |             |    |----------|
               |          |    |             |  |             |    |          |
               | MySQL    |<--+|             |  |             |+-->| MySQL    |
               |          |    |             |  |             |    |          |
               +----------+    +-------------+  +-------------+    +----------+

What do we need?

Install the following software:

  • Apache httpd
  • Module mod_security
  • Module mod_headers
  • Module mod_proxy, mod_proxy_ajp, mod_proxy_http
  • Module mod_rewrite
  • Module mod_ssl

Using Debian Linux:

sudo apt-get install apache2.2 apache22-mpm-worker libapache-mod-security
sudo a2enmod mod-security headers proxy proxy_ajp proxy_http rewrite ssl

The setup

I used standard paths for configuration, like /etc/apache2 and /var/log/apache2.

Apache starts by reading apache2.conf which includes other configuration files. The ASCII graphics below show what is included and in what order, watch for the numbers.

apache2.conf

+ /etc/apache2
|
+--- apache2.conf    <-1------ Main configuration, includes
+--- httpd.conf      <-3----------------------------/  ||||
+--- ports.conf      <-4------------------------------/ |||
|                                                       |||
+--- conf.d/         <-5--------------------------------/||
|    |                                                   ||
|    +--- mod-security                                   ||
|                                                        ||
+--- mods-enabled/   <-2---------------------------------/|
|    |                                                    |
|    +--- headers.load                                    |
|    +--- mod-security.load                               |
|    +--- proxy.conf                                      |
|    +--- proxy.load                                      |
|    +--- proxy_ajp.load                                  |
|    +--- proxy_http.load                                 |
|    +--- ssl.load                                        |
|                                                         |
+--- sites-enabled/  <-6----------------------------------/
|    |
|    +--- project1.conf    ---- vhost 1, includes --------\
|    +--- project2.conf    ---- vhost 2, includes ------\ |
|                                                       | |
+--- mod-security/                                      | |
|    |                                                  | |
|    +--- lua/                                          | |
|    +--- project2.conf    <----------------------------/ |
|    +--- project1.conf    <------------------------------/

These are the important lines from apache2.conf to review the order when configuration files are loaded:

[...]

# Include module configuration:
Include mods-enabled/*.load
Include mods-enabled/*.conf

# Include all the user configurations:
Include httpd.conf

# Include ports listing
Include ports.conf

[...]

# Include generic snippets of statements
Include conf.d/

# Include the virtual host configurations:
Include sites-enabled/

ports.conf

Add IP address and port to bind in ports.conf:

Listen 80
<IfModule mod_ssl.c>
    Listen 443
</IfModule>
NameVirtualHost 10.1.1.2:80
NameVirtualHost 10.1.1.3:80

mods-enabled/proxy.conf

To use Apache as a reverse proxy for requests, configure mod_proxy. Don't forget to specify ProxyRequests Off as we don't want a forward proxy.

<IfModule mod_proxy.c>
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyVia On
    <Proxy *>
        AddDefaultCharset off
        Order deny,allow
        Allow from all
    </Proxy>
</IfModule>

conf.d/mod_security

To initialize mod_security we load two libraries (LoadFile): libxml and liblua. These two enable the use of programming language Lua and XML parsing. This makes rule management more fun ;-)

Next we enable the rule engine (SecRuleEngine) and define a directory where mod_security can save (temporary) data (SecDataDir) and take care of the privileges (chown, chmod).

To hide the real software and version from others, we want mod_security to use Apache 1.3.41 in the Server: header. Additionally I used mod_headers here, as I had a special case where SecServerSignature wasn't sufficient.

<IfModule mod_security2.c>
    LoadFile /usr/lib/libxml2.so.2
    LoadFile /usr/lib/liblua5.1.so.0
    SecRuleEngine On
    SecDataDir /var/modsec
    ServerTokens Full
    SecServerSignature "Apache 1.3.41"
</IfModule>
<IfModule mod_headers.c>
    Header set Server "Apache 1.3.41"
    Header unset X-Powered-By
</IfModule>

A rule set

The rules for the first application are configured in mod_security/application1.conf. mod_security processes rules in phases, every phase has access to different data:

  • Phase 1: HTTP request headers
  • Phase 2: HTTP request body
  • Phase 3: HTTP response headers
  • Phase 4: HTTP response body
  • Phase 5: Logging (no destructive actions allowed)

First, set an web application id. This is not just an informational thing, it helps to distinguish between different applications secured through one mod_security installation and is used with their cookies.

<IfModule mod_security2.c>
    SecWebAppId "application1"
</IfModule>

Specify settings for logging:

SecAuditEngine RelevantOnly
#SecAuditLogRelevantStatus "^[45]"
SecAuditLogType serial
SecDebugLog /var/log/apache2/application1.modsecurity.log
SecDebugLogLevel 4

Default phase for rule processing is phase:2, when request headers and body are available. We allow and log a request by default and send back HTTP status:404 when a request is denied by some rule to simulate a failed/wrong request. Another strategy would be to use HTTP 500 to show a "crashed" or "buggy" application. The t:ransformation removeNulls is used to remove NULL-bytes and thus prevent a NULL-byte attack. Think of e.g. Java, which does not terminate strings using the NULL-byte but the operating system which is written in C does...

SecDefaultAction "phase:2,allow,status:404,t:removeNulls"

Configure access to the HTTP request body access and set memory limits when parsing data (32 MB in this case):

SecRequestBodyAccess On
SecRequestBodyInMemoryLimit 1048576
SecRequestBodyLimit 8388608
SecResponseBodyLimit 33554432

You can also specify access to HTTP response body, use SecResponseBodyAccess On, this is useful in phase 4.

Phase 1: HTTP request headers

In the first phase mod_security processes just HTTP request headers.

Client IP address

To make decisions relating to the client's IP address, I initialize a collection and save REMOTE_ADDR:

SecAction "phase:1,initcol:ip=%{REMOTE_ADDR},pass"

You can also deny a special IP, using a regular expression:

SecRule REMOTE_ADDR "@rx 192.1.1." "deny"

or by matching a string:

SecRule REMOTE_ADDR "@streq 192.1.1.100" "deny"

Or use geo lookups to control e.g. which country can access the application. First download and unpack MaxMind's GeoLite database:

mkdir -p /usr/local/geoip
curl -L http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz \
  | gzip -c > /usr/local/geoip/GeoLiteCity.dat

And for example deny every access coming from a certain country code, @streq DE:

SecGeoLookupDb "/usr/local/geoip/GeoLiteCity.dat"
SecRule REMOTE_ADDR "@geoLookup" "deny,chain"
    SecRule GEO:COUNTRY_CODE "!@streq DE"

Pay attention: Some company networks use public IP addresses for private space (due to some history), instead of 192.168.0.0/16, 172.16.0.0/12 or 10.0.0.0/8. These could be "accidentally" excluded. Also access to your application is dependent on the information stored in MaxMinds' geo database. This may or may not be to your satisfaction.
You have been warned.

HTTP headers

Maybe you want to control how many (chained) proxy servers are allowed to access the application. I think one should be sufficient as many companies use a proxy:

SecRule &REQUEST_HEADERS:X-Forwarded-For "@gt 1" "deny"

Check the headers for valid content is a good idea. At first list allow header names:

SecRule REQUEST_HEADERS_NAMES \
    "!^(Accept|Referer|Cache-Control|Accept-Language|Accept-Charset|Content-Type|Content-Length|Cookie|User-Agent|Accept-Encoding|Host|Connection|Pragma|If-Modified-Since|If-None-Match|Origin|x-requested-with|X-Requested-With)$" \
    "deny,msg:'Unknown request header'"

And control what input is allowed per HTTP header:

SecRule REQUEST_HEADERS:Accept             "!^[a-zA-Z0-9-.,:;+*/= ]+$" \
    "deny,msg:'Bad Accept header'"
SecRule REQUEST_HEADERS:Referer            "!^[a-zA-Z0-9-.]+$" \
    "deny,msg:'Bad Referer header'"
SecRule REQUEST_HEADERS:Accept-Language    "!^[a-zA-Z0-9-.,:;+*/=]+$" \
    "deny,msg:'Bad Accept-Language header'"
SecRule REQUEST_HEADERS:User-Agent         "!^[-ws*/:;.,()=]+$" \
    "deny,msg:'Bad User-Agent header'"
SecRule REQUEST_HEADERS:Content-Type       "!^[-ws*/:;.,()=]+$" \
    "deny,msg:'Bad Content-Type header'"
SecRule REQUEST_HEADERS:Content-Length     "!^[d]{1,20}$" \
    "deny,msg:'Bad Content-Length header'"
SecRule REQUEST_HEADERS:Accept-Encoding    "!^[-ws*/:;.,()]+$" \
    "deny,msg:'Bad Accept-Encoding header'"
SecRule REQUEST_HEADERS:Host               "!^[a-zA-Z0-9-.]+$" \
    "deny,msg:'Bad Host header'"
SecRule REQUEST_HEADERS:Connection         "!^[-w]+$" \
    "deny,msg:'Bad Connection header'"
SecRule REQUEST_HEADERS:Pragma             "!^[-w*/]+$" \
    "deny,msg:'Bad Pragma header'"
SecRule REQUEST_HEADERS:If-Modified-Since  "!^[a-zA-Z0-9,: ]+$" \
    "deny,msg:'Bad If-Modified-Since header'"
SecRule REQUEST_HEADERS:If-None-Match      "!^[a-zA-Z0-9/\"-]+$" \
    "deny,msg:'Bad If-None-Match header'"
SecRule REQUEST_HEADERS:Cookie             "!^[-ws=*/;]+$" \
    "deny,msg:'Bad Cookie header'"

Time based access

SecRule TIME_HOUR "@lt 18" \
    "deny"
SecRule TIME_HOUR !^(8|9|10|11|12|13|14|15|16|17)$ \
    "deny"

Browser

Allow certain web browsers only, e.g. User-Agent header must contain Mozilla :

SecRule REQUEST_HEADERS:User-Agent "@rx !Mozilla" \
    "deny,msg:'Unsupported browser detected'"

HTTP methods

Block uncommon HTTP request methods. Available methods are:

RFC 2616 "Hypertext Transfer Protocol -- HTTP/1.1"

  • OPTIONS
  • GET
  • HEAD
  • POST
  • PUT
  • DELETE
  • TRACE
  • CONNECT

RFC 2518 "HTTP Extensions for Distributed Authoring -- WebDAV"

  • PROPFIND
  • PROPPATCH
  • MKCOL
  • COPY
  • MOVE
  • LOCK
  • UNLOCK

RFC 3253 "Versioning Extensions to WebDAV"

  • VERSION-CONTROL
  • REPORT
  • CHECKOUT
  • CHECKIN
  • UNCHECKOUT
  • MKWORKSPACE
  • UPDATE
  • LABEL
  • MERGE
  • BASELINE-CONTROL
  • MKACTIVITY

RFC 3648 "WebDAV - Ordered Collections Protocol"

  • ORDERPATCH

RFC 3744 "WebDAV - Access Control Protocol"

  • ACL

draft-dusseault-http-patch

  • PATCH

draft-reschke-webdav-search

  • SEARCH

For most applications it should be ok to deny everything except GET, POST and HEAD:

SecRule REQUEST_METHOD "@rx !^(GET|POST|HEAD)$" \
    "deny,status:405"

Directory traversal

    SecRule REQUEST_URI "@streq ../" \
        "t:urlDecode,deny"

File downloads

SecRule REQUEST_URI "@contains streamFile.do" \
    "t:urlDecode,deny,chain"
    SecRule ARGS:fileName "!@contains D:\Infoniqa\Tomcat 7.0\temp"

Virus Scanning

SecRule ARGS "virus" "setenv:SEARCH='INTESIV',exec:'/usr/bin/virenscanner.pl'"

Sensitive information

Deny some URLs revealing sensitive information

SecRule REQUEST_URI "ENGAGEINFO_SYSTEM|ENGAGEINFO_VERSION|ENGAGEINFO_LICENSE" \
    "proxy:http://localhost/engageinfo.html"

Phase 2

This phase has access to everything from phase 1 and HTTP request body.

Command Execution

This rule matches too often

SecRule ARGS "^(rm|ls|kill|(send)?mail|cat|echo|/bin/|/etc/|/tmp/)[[:space:]]" \
    "deny"

SQL Injection

SecRule ARGS "unions+select" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "unions+alls+select" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "intos+outfile" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "drops+table" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "alters+table" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "load_file" \
    "t:lowercase,deny,msg:'SQL Injection'"
SecRule ARGS "selects+" \
    "t:lowercase,deny,msg:'SQL Injection'"

HTML tags

SecRule ARGS "<[[:space:]]*script" \
"t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element script not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*meta*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element meta not allowed'"
#SecRule ARGS "<[[:space:]]*[^>]*style*\"?[^>]*>" \
#    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element style not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*script*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element script not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*iframe*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element iframe not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*object*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element object not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*img*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element img not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*applet*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element applet not allowed'"
SecRule ARGS "<[[:space:]]*[^>]*form*\"?[^>]*>" \
    "t:htmlEntityDecode,proxy:http://localhost/error.html,msg:'HTML element form not allowed'"

Phase 3

In this phase the HTTP response headers are available.

Phase 4

In addition to the previous phase, now also the HTTP response body is available.

Prevent directory listings from accidentally being returned

SecRule REQUEST_URI "/$" \
    "phase:4,deny,chain,msg:'Directory index returned'"
    SecRule RESPONSE_BODY "<h1>Index of /"

Source Code revelation

Do not show source code to client. Depending on the language, detect this by checking against typical signs, tags like <? or <% or shebang:

# Prevent PHP source code from being disclosed
SecRule RESPONSE_BODY "<?" \
    "deny,msg:'PHP source code disclosure blocked'"
#
# Prevent Perl source code from being disclosed
SecRule RESPONSE_BODY "#!/usr/bin/perl" \
    "deny,msg:'Perl source code disclosure blocked'"
#
# Prevent JSP source code from being disclosed
SecRule RESPONSE_BODY "@streq <%" \
   "deny,msg:'JSP source code disclosure blocked'"

Detect repeated, failed logins

Block further login attempts after 3 failed attempts for 60 seconds. Additionally check the username when logging in, it should only contain upper and lower case character, the numbers 0 to 9 and special characters -, ., _.

<LocationMatch ^/application1/login>
    SecRule RESPONSE_BODY "Invalid login" \
        "phase:4,setvar:ip.failed_logins=+1,expirevar:ip.failed_logins=60,pass"
    SecRule ARGS:userName "@validateByteRange 45, 46, 95, 48-57, 64, 65-90, 97-122" \
        "setvar:ip.failed_logins=+1,expirevar:ip.failed_logins=60,pass,log"
    SecRule IP:FAILED_LOGINS "@gt 2" \
        "proxy:http://localhost/your-ip-is-banned.html,msg:'num=%{ip.failed_logins}'"
</LocationMatch>

HTH.

This entry was posted in Security, System Administration and tagged , . Bookmark the permalink.