Rate limiting GraphQL API query using iRule and Advanced WAF Violation
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:
- Clinet IP A - Query X - Limit 1
- Clinet IP A - Query Y - Limit 2
- Client IP A - Query Z - Limit 3
Solution
The proposed solution:
- iRule for finding Query sting from the payload of “POST” method, apply rate limit based on data-group and raise ASM Custom violation
- Advanced WAFM configuration
- User-defined violation with custom settings
- Security policy enabled with “Trigger ASM iRule Events Mode” set to normal and in “Learning and Blocking Settings” custom violation is enabled for “Alarm” and “Block”
- Data-group for defining rate-limit per query string
Configuration:
- Advanced WAF Custom Violation and Policy settings:
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
- Create data-group
**** data-group ***
ltm data-group internal query-rps {
records {
Login {
data 4
}
getOrder {
data 3
}
getPatientByMobileNumber {
data 6
}
}
type string
}
- Create iRule to refer data-group for rate-limiting and use user-defined violation when rate-limiting is crossed by client (this iRule is compatible for XFF too):
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"
}
}
- Attach this iRule and Security policy to desired Virtual server
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.