19-Jul-2022 05:00 - edited 19-Jul-2022 10:30
Introduction
F5 TMOS have many features and functions which can be used to achieve different requirements in case it is not available in config UI. This DevCentral article provides details on how to implement rate limiting based on Query String (per different query) in GraphQL queries using a combination of iRule and Advanced WAF custom violation (for common security event logs).
Problem statement
Similarly to REST API, GraphQL API is usually served over HTTP and is prone to the typical Web APIs security vulnerabilities, such as injection attacks, Denial of Service (DoS) attacks and abuse of functionality.
BIG-IP APM supports rate-limiting API requests but it doesn’t support GraphQL Schema. The newly released F5 BIG-IP versions have Advanced WAF - GraphQL Security policy template which supports a generic limit on batched queries, however, this policy template and content policy are not flexible to achieve rate limiting per different query.
Requirement is rate limiting should be apllied per client IP address and query combination. For example:
Solution
The proposed solution:
Configuration:
Create user-defined custom violation under Security >> Options >> Application Security >> Advanced Configuration >> Violation List
Violation Settings:
Set security policy to use this violation:
Set security policy to trigger iRule event
**** data-group ***
ltm data-group internal query-rps {
records {
Login {
data 4
}
getOrder {
data 3
}
getPatientByMobileNumber {
data 6
}
}
type string
}
when RULE_INIT {
set static::maxReqs 4
set static::timeout 2
}
when HTTP_REQUEST {
set reqhost [HTTP::host]
set lreqhost [string tolower $reqhost]
set requri [HTTP::uri]
set reqsplit 0
set reqBlock 0
HTTP::header remove "Accept-Encoding"
if { [HTTP::method] equals "POST" } {
set reqsplit 1
# log local0. "POST Request"
HTTP::collect [HTTP::header Content-Length]
}
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]
}
}
when HTTP_REQUEST_DATA {
if { $reqsplit } {
if { [HTTP::payload] contains "query"} {
log local0. "HTTP Payload contains Query = [HTTP::payload] "
set qpayload [getfield [string range [HTTP::payload] [expr { [string first "query" [HTTP::payload]] + 14 }] end] "(" 1]
# set payload [string trim [HTTP::payload] ":" ]
set qrps [class lookup $qpayload query-rps]
if { $qrps equals ""} {
set qrps 10
}
log local0. "HTTP Request data = $cIP_addr$qpayload and Q-RPS = $qrps"
# HTTP::respond 200 content "OKey"
set getcount [table lookup -notouch $cIP_addr$qpayload]
if { $getcount equals "" } {
table set $cIP_addr$qpayload "1" $static::timeout $static::timeout
} else {
if { $getcount < $qrps } {
# log local0. "Request Count for $cIP_addr is $getcount"
table incr -notouch $cIP_addr$qpayload
} else {
set reqBlock 1
# HTTP::respond 403 content {
# <html>
# <head><title>HTTP Request denied</title></head>
# <body>Your HTTP requests are being throttled.</body>
# </html>
# }
}
}
}
HTTP::release
}
}
when ASM_REQUEST_DONE {
if { $reqBlock == 1 } {
ASM::raise RATE-LIMIT-CROSSED
log local0. "ASM have raised RATE-LIMIT-CROSSED"
}
}
Remarks:
This iRule is tuned to search the first occurrence of “query” string in the payload and read 14th character onward till opening parentheses. My sample payload have data like:
"query":"query Login($mobileNumber:
In case Advanced WAF is not licensed and required to force rate limiting with just LTM license, please hash out HTTP::respond and remove ASM_REQUEST_DONE event context.