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