HTTP slow post mitigation
Problem this snippet solves:
Here is an updated version of the iRule from this post and techtip.
Mitigating Slow HTTP Post DDoS Attacks With iRules – Follow-up https://devcentral.f5.com/Community/GroupDetails/tabid/1082223/asg/50/aft/1174748/showtab/groupforums/Default.aspx#1200083
This past week researchers demonstrated a new HTTP DDoS attack in which a slow POST request will result in leaving a connection open longer than necessary. The heart of the attack relies on sending a POST request with given “content-length” then very slowly sending the POST message body to the server. The server will leave the connection open as it continues to receive data. If a large number of these requests are executed against a server, there is potential for exhausting the connection table leaving the server unable to respond to further requests.
There are a few different ways to mitigate such an attack using iRules. You could build a list of all the URLs that require POST requests and only allow requests for these URLs. This method while effective against a blanket attack would prove ineffective against an attacker who was targeting a page that relies on legitimate POST requests. Aside from that, someone must maintain the list. Enter the “after” command…
The after command was introduced in version 10 of BIG-IP and allows among other things, the cancellation of a previous script. In this example, if our client is sending their POST request too slowly we will instruct our BIG-IP to send them a HTTP 500 error and close the connection after a given period. If their request is received within the allotted time period, we will cancel our previous instruction and allow the connection to them proceed to the origin servers.
Code :
# Based on 'Mitigating Slow HTTP Post DDoS Attacks With iRules' from George Watkins # # Requires LTM v10.0+ for the after command when RULE_INIT { # This iRule enforces a minimum length of time ($static::timeout) for a client to send POST request data. # The initial values are 2Kb / 2 sec = 1 Kb/s for the first 2Kb. These values should be tailored for the client base. # Default amount of request payload to collect (in bytes). # This is the maximum amount of content we will collect. # Clients will still be able to send unlimited payload sizes. set static::collect_length 2048 # Default timeout, for POST requests, to send $collect_length bytes (in milliseconds) set static::timeout 2000 # HTML response to send for blocked requests set static::block_html {Your POST request is not being received quickly enough. Please retry.} # Log debug messages to /var/log/ltm? 1=yes, 0=no. set static::post_debug 1 } when HTTP_REQUEST { # Only check POST requests. If the application supports other request methods with payloads, add them in a switch statement here. if { [HTTP::method] equals "POST"} { if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: POST to [HTTP::host][HTTP::uri]" } # Create a local variable copy of the collection amount set collect_length $static::collect_length # Create a local variable copy of the static timeout set timeout $static::timeout # Check for a non-existent Content-Length header if {[HTTP::header Content-Length] eq ""}{ # Use default collect length for POSTs without a Content-Length header set collect_length $static::collect_length if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: No Content-Length value" } } elseif {[HTTP::header Content-Length] <= 0}{ # Don't try to collect a payload if there isn't one unset collect_length if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Content-Length: 0." } } elseif {[HTTP::header Content-Length] > $static::collect_length}{ # Use the default collect length set collect_length $static::collect_length if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Content-Length: [HTTP::header Content-Length], collecting $collect_length" } } else { # Collect the actual payload length set collect_length [HTTP::header Content-Length] # Calculate a custom timeout based on the same ratio we use for the default collect length and default timeout set timeout [expr {[HTTP::header Content-Length] * $static::timeout / $static::collect_length }] if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Content-Length: [HTTP::header Content-Length], collecting $collect_length bytes with timeout $timeout ms" } } # If the POST Content-Length isn't 0, collect (a portion of) the payload if {[info exists collect_length]}{ # If the entire request hasn't been received within X seconds, send a 408, and close the connection set id [after $timeout { if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: $timeout ms reached. Closing connection" } HTTP::respond 408 content $static::block_html Connection Close TCP::close }] # Trigger collection of the request payload HTTP::collect $collect_length if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Collecting $collect_length" } } } } when HTTP_REQUEST_DATA { if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Collected [HTTP::payload length] bytes." } # Check if the 'after' ID exists if {[info exists id]} { # If all the POST data has been received, cancel the connection closure if { $static::post_debug } { log local0. "[IP::client_addr]:[TCP::client_port]: Canceling \$id: $id" } after cancel $id # This needs to be reworked to address ID454692 # unset id } }
- sthussey_131917NimbostratusAre there any concerns about race conditions for Keep-Alive connections? It seems the 'id' variable is scoped to the TCP connection. If we get request A and begin data collection (and therefore initiated the 'after' call with id 1) and before the collection is finished, we receive request B and collection is started and another 'after' call is started with id 2. When HTTP_REQUEST_DATA is called for the collection of request A, wouldn't $id now be 2 and therefore the cancellation call for the after event 1 would not happen. Could this cause termination of valid connections in some edge cases? Edit: I over thought it. HTTP 1.1 doesn't allow concurrent outstanding requests over a single connection, so no race condition.