Friday, May 27, 2016

ModSecurity Anti-Automation Rule Set for Detecting Denial of Service (DOS) Attacks, Part 2

Continuing my experiment with ModSecurity on a web server running in detection mode, I was discovering that a few of my users were triggering false alarms by the anti-DOS rules even though their number of submitted requests is well below the specified burst threshold of 100 requests per minute.  While searching online for a solution, I found that I was not the only one encountering this issue.  It seems that the false alarm occurs whenever web requests are submitted concurrently to the server by a user.  If the requests are submitted sequentially, the rules work as expected.  Unfortunately, I didn't find anybody posting a fix to the rules and so I took a trip through the tunnel...


Race Condition


The crux of the problem seems to be rule 981048:

#
#
# Check DOS Counter
# If the request count is greater than or equal to user settings, we then set the burst counter
# 
SecRule IP:DOS_COUNTER "@ge %{tx.dos_counter_threshold}" "phase:5,id:'981048',t:none,nolog,pass,setvar:ip.dos_burst_counter=+1,expirevar:ip.dos_burst_counter=%{tx.dos_burst_time_slice},setvar:!ip.dos_counter"


The rule does not operate atomically so rule operations such as unset(ip.dos_counter) are not guaranteed to finish first before the rule is executed by another request.  Consequently, when multiple concurrent requests activate the rule at the same time with the rule condition met, ip.dos_burst_counter is incremented by one for each concurrent request instead of incremented by one for all concurrent requests.  This means ip.dos_burst_counter can jump from 0 to ≥ 2.  Erroneously, the following rule 981049 then blocks the user because ip.dos_burst_counter is ≥ 2.

To illustrate with an example, a user has sent 99 requests slowly and regularly over time, so ip.dos_counter is now at 99 while ip.dos_burst_counter is 0.  All of the sudden, he sends 2 requests simultaneously.  Because the rule is not atomic, ip.dos_burst_counter is incremented twice and is now set to 2.  Had the 2 requests arrived sequentially, ip.dos_burst_counter would be 1 because ip.dos_counter would have been unset properly after the first request.  Now, because ip.dos_burst_counter is 2, the user is blocked even though he had not burst-ed.


The Solution


To fix the problem, I propose replacing rule 981048 with two chained rules to ensure ip.dos_burst_counter doesn't skip from 0 to ≥ 2.

#
# Check DOS Counter (Tick)
# If the request count is greater than or equal to user settings, we then set the burst counter to 1 if it's 0
# 
SecRule IP:DOS_COUNTER "@ge %{tx.dos_counter_threshold}" "chain,phase:5,id:'981048',t:none,nolog,pass"
SecRule &IP:DOS_BURST_COUNTER "@eq 0" "setvar:ip.dos_burst_counter=1,expirevar:ip.dos_burst_counter=%{tx.dos_burst_time_slice},setvar:!ip.dos_counter"

#
# Check DOS Counter (Tock)
# If the request count is greater than or equal to user settings, we then set the burst counter to 2 if it's 1
# 
SecRule IP:DOS_COUNTER "@ge %{tx.dos_counter_threshold}" "chain,phase:5,id:'981050',t:none,nolog,pass"
SecRule &IP:DOS_BURST_COUNTER "@ge 1" "setvar:ip.dos_burst_counter=2,expirevar:ip.dos_burst_counter=%{tx.dos_burst_time_slice},setvar:!ip.dos_counter"


For the previous example, the "tick" rule ensures ip.dos_burst_counter is set to at most 1 even if the rule is executed concurrently.  The "tock" rule dictates the condition that ip.dos_burst_counter can only be set to 2 if it was previously set to 1.  Together, the rules simulate the operation of the original rule while able to handle concurrency without triggering false positives. As for false negatives, the rules may trigger slightly later than the specified threshold crossing (e.g. 100 requests per minute) for concurrent requests due to the inherit lack of atomicity in ModSecurity.

No comments:

Post a Comment