ASM Sanitize Blocking Page Requests

Problem this snippet solves:

This rule performs validation and sanitization of requests to the error page hosted on an ASM-enabled virtual server to ensure requests which were blocked and redirected by ASM to the error page aren't themselves blocked. iRule Methodology: 1. The rule only performs validation if the request is for the error page. The error page needs to be manually set in ::blocking_page in the RULE_INIT event. 2. All non-GET requests to the error page are redirected to the blocking page with all URI parameters removed, except the "error" parameter. 3. The error parameter is checked to ensure it only contains 1 to 20 digits. If it doesn't, the value is replaced with a default 20 digit string. 4. All parameters in the URI, except "error" are removed. 5. All HTTP headers except those defined in ::headers_to_preserve in the RULE_INIT event are removed. 6. If the Host header contains illegal characters, the request is dropped. The following are considered valid Host characters: A-Z a-z _ . :

How to use this snippet:

1. The error page must be configured in the RULE_INIT event (::blocking_page) 2. The error parameter name must be configured in the RULE_INIT event (::error_parameter) 3. The headers to keep in the request must be configured in the RULE_INIT event (::headers_to_preserve) 4. The blocking response in the ASM security policy should be set to redirect to the full URL for the virtual server, with a query string of

Code :

# Sanitize requests for the web application's error page
#
# BIG-IP ASM
#   Tested on versions 9.2.4 and 9.4.0
#
# Modified: 
#    14 Jan 2008 - Aaron Hooley - LODOGA Security Limited (hooleylists at gmail dot com)
#
# Version: 1.4
#
# Description: 
#    This rule performs validation and sanitisation of requests to the error page hosted on an ASM-enabled virtual server 
#       to ensure requests which were blocked and redirected by ASM to the blocking page aren't themselves blocked.
#
# Functionality:
#
#   1. The rule only performs validation if the request is for the error page.  The error page needs to be manually set in ::blocking_page in the RULE_INIT event.
#
#   2. All non-GET requests to the error page are redirected to the blocking page with all URI parameters removed, except the "error" parameter.
#
#   3. The error parameter is checked to ensure it only contains 1 to 20 digits.  If it doesn't, the value is replaced with a default 20 digit string.
#
#   4. All parameters in the URI, except "error" are removed.
#
#   5. All HTTP headers except those defined in ::headers_to_preserve in the RULE_INIT event are removed.
#
#   6. If the Host header contains illegal characters, the request is dropped.  The following are considered valid Host characters: A-Z a-z _ . :
#
# Configuration requirements:
#
#    1. The error page must be configured in the RULE_INIT event (::blocking_page)
#
#    2. The error parameter name must be configured in the RULE_INIT event (::error_parameter)
#
#    3. The headers to keep in the request must be configured in the RULE_INIT event (::headers_to_preserve)
#
#    4. The blocking response in the ASM security policy should be set to redirect to the full URL for the virtual server, 
#       with a query string of:
#
#          ?error=<%TS.request.ID()%>
#
#       where "error" is the error parameter name configured in this rule.

when RULE_INIT {

   # Set the error page name using the full path.  Example: "/path/to/my.error.asp"
   set ::blocking_page "/path/to/error.php"

   # Set error parameter name.  This is what the support ID is set as in the ASM blocking response.  Example: error
   set ::error_parameter "error"

   # Default error number.  This is used if the error parameter is blank, but the request is otherwise valid.
   set ::default_error_number 10000000001000000000

   # These are the headers which will not be removed from requests to the blocking page
   #   (need to append a \ to the end of the header name to escape the new line)
   set ::headers_to_preserve [list \
      Host \
      Accept \
      Cookie \
      Connection \
      Proxy-Connection \
      Content-Type \
      Transfer-Encoding \
   ]

   # Set to 1 to log debug messages to /var/log/ltm.  1=yes, 0=no
   set ::sanitise_debug 1
   
   if {$::sanitise_debug}{log local0. "Debug enabled"}
}
when HTTP_REQUEST {

   # Is request for the blocking page?  
   if {[string match -nocase [HTTP::path] $::blocking_page]}{

      # Debug logging
      if {$::sanitise_debug}{ log local0. "client [IP::client_addr] -> [HTTP::host][HTTP::uri] matched blocking page check"}
 
      # Save the error parameter value to a variable for validation
      set error_value [URI::query [HTTP::uri] $::error_parameter]

      # Check if the error parameter value has a length and is numeric
      if {[string length $error_value] and $error_value matches_regex {^[0-9]{1,20}$}}{

         # Error value was valid, so don't modify it

      } else {

         # Error value wasn't set, or was set to a non-numeric value.  Set it explicitly to the default.
         set error_value $::default_error_number
      }
      

      # Is request made via GET?  Redirect all non-GET requests to the same page with the error parameter set.
      if {[HTTP::method]=="GET"}{

         # Debug logging
         if {$::sanitise_debug}{ log local0. "client [IP::client_addr] -> [HTTP::host][HTTP::uri] matched method check [HTTP::method]"}

         # Make sure ASM will receive the request by default
         set asm_bypass 0

         # Debug logging
         if {$::sanitise_debug}{log local0. "Original URI: [HTTP::uri]"}


         # Error parameter value has been validated, so set the URI to just the path plus the error parameter and its value. 
         #   Remove all other query string parameters
         HTTP::uri "${::blocking_page}?${::error_parameter}=$error_value"

         # Debug logging
         if {$::sanitise_debug}{log local0. "Updated URI: ${::blocking_page}?${::error_parameter}=$error_value"}

         # Log original header names, if debug is enabled
         if {$::sanitise_debug}{
            foreach aHeader [HTTP::header names] {
               log local0. "Original header: $aHeader"
            }
         }

         # Remove all headers but those in the preserve list header
         foreach one_header [HTTP::header names] {
            if {not ([matchclass $::headers_to_preserve equals $one_header])}{
               while {[HTTP::header exists $one_header]}{
                  if {$::sanitise_debug}{log local0. "Removing: $one_header: [HTTP::header value $one_header]"}
                  HTTP::header remove $one_header
               }
            }
         }
         # Log updated headers, if debug is enabled
         if {$::sanitise_debug}{
            foreach one_header [HTTP::header names] {
               log local0. "Updated header: $one_header: [HTTP::header value $one_header]"
            }
         }

         # Check that the Host header value contains only 'alphanumeric, underscore, period or colon'. If not, drop the request (and disable ASM for this request). 
         if { [HTTP::host] matches_regex {^[A-Za-z0-9_.]+$} }{

            # Request was valid, so enable ASM for the request
            set asm_bypass 0

         } else {

            # Request had an invalid character in the Host header value, so bypass ASM and drop it.

            log local0. "Dropping request from client [IP::client_addr] to \
               [IP::local_addr][HTTP::uri] because Host contained illegal metacharacters: [HTTP::host]" 

            # Disable ASM as we're dropping the request
            set asm_bypass 1

            # Drop the request
            drop
         }

      } else {

         # Request wasn't made via GET, so redirect to the same host with just the error parameter set

         # Disable ASM for this request as we're redirecting the client
         set asm_bypass 1

         # Determine if the request was made via HTTPS to use correct protocol for the redirect. 
         #     Use a workaround to include SSL::disable with no client/server SSL profile (C356200: SSL:: commands require client/server SSL profile)
         set sslCmd "SSL::mode"

         if {[PROFILE::exists clientssl]==1 && [eval $sslCmd]==1}{

            # Send redirect to https, with only the error parameter and value set
            HTTP::redirect https://[HTTP::host]$::blocking_page?$::error_parameter=$error_value
            if {$::sanitise_debug}{log local0. "Redirecting request from client [IP::client_addr] made to [IP::local_addr][HTTP::uri]\
               to https://[HTTP::host]$::blocking_page?$::error_parameter=$error_value because request method was [HTTP::method]"}

         } else {
            # Send redirect to http, with only the error parameter and value set
            HTTP::redirect http://[HTTP::host]$::blocking_page?$::error_parameter=$error_value
            if {$::sanitise_debug}{log local0. "Redirecting request from client [IP::client_addr] made to [IP::local_addr][HTTP::uri]\
               to http://[HTTP::host]$::blocking_page?$::error_parameter=$error_value because request method was [HTTP::method]"}
         }
      }
   }
}

Tested this on version:

9.2
Published Jan 30, 2015
Version 1.0

Was this article helpful?