ASM/WAF rate limit and block clients by source ip (or device_id fingerprint) if there are too many violations
Here is an example with failover to using the client ip address if ASM fingerprint if it is equal to "0" (no device ID fingerprint available). As I mentioned better use the main irule for blocking by source IP address or for device fingerprint the session tracking and awareness (https://support.f5.com/csp/article/K02212345).
I just played with this as the ASM fingerprint is not aways generated for a web request and you need to first enable a feature like web scraping or in the newer versions the unified bot defense (see https://devcentral.f5.com/s/question/0D51T00006kGZft/asm-fingerprinting-with-irule and the general articles https://support.f5.com/csp/article/K19556739 ), so this irule can be helpfull if the default F5 features that use the fingerprint don't failover to client source IP address if there is no available fingerprint.
I also created 2 subtables for the ip addresses and fingerprints for optimizing the lookups.
when RULE_INIT {
# The max requests served within the timing interval per the static::timeout variable
set static::maxReqs 2
# Timer Interval in seconds within which only static::maxReqs Requests are allowed.
# (i.e: 10 req per 2 sec == 5 req per sec)
# If this timer expires, it means that the limit was not reached for this interval and
# the request counting starts over. Making this timeout large increases memory usage.
# Making it too small negatively affects performance.
set static::timeout 20
}
#when CLIENT_ACCEPTED {
#set cIP_addr [IP::client_addr]
# set getcount [table lookup -subtable "bad_ip" -notouch $cIP_addr]
# if { ! ( [class match $cIP_addr equals ip_whitelist] ) } {
# The following expects the IP addresses in multiple X-forwarded-for headers.
# It picks the first one. If XFF isn’t defined it can grab the true source IP.
# if { $getcount > $static::maxReqs } {
# log local0. "Request Count for $cIP_addr is $getcount"
# drop
# }
# }
#}
when HTTP_REQUEST {
# Allows throttling for only specific URIs. List the URIs_to_throttle in a data group.
# Note: a URI is everything after the hostname: e.g. /path1/login.aspx?name=user
if { [HTTP::header exists X-forwarded-for] } {
set cIP_addr [getfield [lindex [HTTP::header values X-Forwarded-For] 0] "," 1]
} else {
set cIP_addr [IP::client_addr]
}
set getcount1 [table lookup -subtable "bad_ip" -notouch $cIP_addr]
if { ! ( [class match $cIP_addr equals ip_whitelist] ) } {
# The following expects the IP addresses in multiple X-forwarded-for headers.
# It picks the first one. If XFF isn’t defined it can grab the true source IP.
if { $getcount1 >= $static::maxReqs } {
log local0. "Request Count for $cIP_addr is $getcount1"
# drop
HTTP::respond 403 content {
<html>
<head><title>HTTP Request denied</title></head>
<body>Your HTTP requests are being blocked because of too many violations.</body>
</html>
}
}
}
}
when ASM_REQUEST_DONE {
log local0.debug "\[ASM::status\] = [ASM::status] and ASM_Device_id = [ASM::fingerprint] "
if { [ASM::status] equals "blocked" } {
# Allows throttling for only specific URIs. List the URIs_to_throttle in a data group.
# Note: a URI is everything after the hostname: e.g. /path1/login.aspx?name=user1
if { not ( [class match [IP::client_addr] equals whitelist] ) } {
# The following expects the IP addresses in multiple X-forwarded-for headers.
# It picks the first one. If XFF isn’t defined it can grab the true source IP.
set cIP_finger [ASM::fingerprint]
if { $cIP_finger == 0 } {
if { $getcount1 equals "" } {
table set -subtable "bad_ip" $cIP_addr "1" $static::timeout $static::timeout
# Record of this session does not exist, starting new record
# Request is allowed.
} else {
log local0. "Request Count for $cIP_addr is $getcount1"
table incr -subtable "bad_ip" -notouch $cIP_addr
# record of this session exists but request is allowed.
}
} else {
set getcount2 [table lookup -subtable "bad_id" -notouch $cIP_finger]
if { $getcount2 equals "" } {
table set -subtable "bad_id" $cIP_finger "1" $static::timeout $static::timeout
# Record of this session does not exist, starting new record
# Request is allowed.
} else {
if { $getcount2 >= $static::maxReqs } {
log local0. "Request Count for $cIP_finger is $getcount"
table incr -subtable "bad_id" -notouch $cIP_finger
# record of this session exists but request is allowed.
} else {
#drop
HTTP::respond 403 content {
<html>
<head><title>HTTP Request denied</title></head>
<body>Your HTTP requests are being throttled.</body>
</html>
}
}
}
}
}
}
}