Mitigating Slow HTTP Post DDoS Attacks With iRules – Follow-up
Last month I posted a Tech Tip using iRules to mitigate the slow POST DDoS attack. The example that I posted was an early prototype that was passed around an internal mailing list. I listed a few “gotchas” in my original post, but it wasn’t long until the folks started chatting about it and had an improved implementation.
A couple of the limitations mentioned with the first solution were the 4MB TMM payload collection ceiling and not using the Content-Length header to determine payload collection size. Hoolio took the original iRule I posted and added in logic (forum post) to accomplish both of these. There are two static variables that are set upon rule initiation: the default content length (static::content_length) and the timeout for a slow post (static::timeout). The static::content_length variable should be set to be slightly larger than the largest POST payload you expect your application to accept up to just shy of 4MB. If you have large files that rely on POST requests for uploads, you’ll want to build in additional logic to guard against attacks and further verify legitimate requests. Secondly, the timeout is the maximum amount of time we want to allow a client to fulfill their POST request. If your application supports people with a variety of connection speeds and quality, 2 seconds might be a bit short and may end up disrupting legitimate users. Tune the timeout according to your user base.
Next, as a request arrives at the LTM and the iRule is triggered, we will identify the request type. If it is a POST request, we’ll begin processing the HTTP payload collection logic. In the event that the Content-Length is null or larger than our default value, we’ll set our payload collection length to the default content length value. If it is zero, we won’t collect anything. Finally, if it is a reasonable value within our established payload collection bounds, we’ll collect the length specified by the Content-Length header and set the timeout as a ratio of the Content-Length value and the maximum default collect length (2048 bytes in this case).
The next portion of the rule proceeds much like my first example. If the content_length variable has been set, we start the timer and start collecting. If we don’t collect all the payload data within the allotted time, then we respond with a HTTP status code 408 – request timeout (thanks hoolio for correcting me with a more appropriate response code) and close the connection. If the data is collected in a timely manner, we cancel the connection closure. Put that all together and here’s what you’ve got:
1: when RULE_INIT {
2: # Default amount of request payload to collect (in bytes)
3: set static::collect_length 2048
4:
5: # Default timeout for POST requests to send $collect_length bytes (in seconds)
6: set static::timeout 2
7: }
8:
9: when HTTP_REQUEST {
10: # Only check POST requests
11: if { [HTTP::method] equals "POST"} {
12: # Create a local variable copy of the static timeout
13: set timeout $static::timeout
14:
15: # Check for a non-existent Content-Length header
16: if {[HTTP::header Content-Length] eq ""}{
17: # Use default collect length of 2k for POSTs without a Content-Length header
18: set collect_length $static::collect_length
19: } elseif {[HTTP::header Content-Length] == 0}{
20: # Don't try collect a payload if there isn't one
21: unset collect_length
22: } elseif {[HTTP::header Content-Length] > $static::collect_length}{
23: # Use default collect length
24: set collect_length $static::collect_length
25: } else {
26: # Collect the actual payload length
27: set collect_length [HTTP::header Content-Length]
28:
29: # Calculate a custom timeout based on the same ratio we use for the default collect length and default timeout
30: set timeout [expr {[HTTP::header Content-Length] / $static::collect_length * $static::timeout}]
31: }
32:
33: # If the POST Content-Length isn't 0, collect (a portion of) the payload
34: if {[info exists collect_length]}{
35: # If the entire request hasn't been received within X seconds, send a 408, and close the connection
36: set id [after $timeout {
37: HTTP::respond 408 content "Your POST request is not being received quickly enough. Please retry."
38: TCP::close
39: }]
40:
41: # Trigger collection of the request payload
42: HTTP::collect $collect_length
43: }
44: }
45: }
46:
47: when HTTP_REQUEST_DATA {
48: # Check if the 'after' ID exists
49: if {[info exists id]} {
50: # If all the POST data has been received, cancel the connection closure
51: after cancel $id
52: }
53: }
- mdicarlo_60024NimbostratusThe timeout should be in milliseconds. So instead of 2 it should be 2000.
- cweeklund_18634Nimbostratusi'm a bit confused on the timeout value. mdicarlo says it should be 2000, how do you know it's supposed to be in ms?
- hooleylistCirrostratusHere's an updated version which corrects a few minor issues with the example iRule:
- hooleylistCirrostratusHere's an updated version in the codeshare:
 
 
https://clouddocs.f5.com/api/irules/HTTP-slow-post-mitigation.html - Masterbaker_119NimbostratusHmm. In it's current form and with default values, wouldn't it be trivial for an attacker to just POST the first 2kb of data at a decent speed so not to trigger the irule, then slow down the post rate, therefore rendering this useless?
- whswhswhs124_98Nimbostratus'
- LawrenceNimbostratus
Hi George,
I have used your iRule and it sure mitigate the slow http vulnerability but it has created a lot more Path Based Vulnerability.