HTTP delay and validate clients using Javascript and cookies when CPU is overloaded¶
Contributed by: MikeLowell - m.lowell at f5.com¶
Description¶
If CPU usage is over $::maxcpu (default 60%), send the client
javascript that causes the client to delay a minimum of $::basedelay
milliseconds (default 5000ms, i.e. 5 seconds), plus a random amount of
additional time between 0 and $::basedelay. After the delay, set a
session cookie named $::cookie (default “bigverify”) with a random
token (taken from $::token), and redirect the client back to the same
virtual server (all this is done via client-side javascript). When the
client comes back after the redirect, their cookie is checked against
$::token and if it matches the iRule returns (which causes the pool
associated with the virtual server to be used), otherwise the iRule
sends back the same javascript as though the client was new (i.e. same
as not providing a cookie). Clients that don’t support javascript
would be unable to access the site if this iRule is engaged (based on
exceeding $::maxcpu).
iRule Source¶
# What does this iRule do?
# If CPU usage is over $::maxcpu (default 60%), send the client javascript that
# causes the client to delay a minimum of $::basedelay milliseconds (default
# 5000ms, i.e. 5 seconds), plus a random amount of additional time between 0 and
# $::basedelay. After the delay, set a session cookie named $::cookie (default
# "bigverify") with a random token (taken from $::token), and redirect the
# client back to the same virtual server (all this is done via client-side
# javascript). When the client comes back after the redirect, their cookie is
# checked against $::token and if it matches the iRule returns (which causes the
# pool associated with the virtual server to be used), otherwise the iRule sends
# back the same javascript as though the client was new (i.e. same as not
# providing a cookie). Clients that don't support javascript would be unable to
# access the site if this iRule is engaged (based on exceeding $::maxcpu).
# How does this iRule work?
# The token given to the client is created by encrypting a secret (stored in
# $::secret) with AES using a key (stored in $::key) that changes every
# $::session_timeout seconds (default 600, i.e. 10 minutes). The token changes
# regularly to make it harder for attackers to defeat the protection mechanism
# (i.e. this prevents the attacker from simpling recording a single valid toke
# and reusing it forever).
# The previous token is saved (as $::prev_token) so that it can be compared
# against client requests in case the client doesn't provide the current token
# (this way regularly scheduled token changes and iRule modifications don't
# cause existing client sessions to fail verification). This also means that a
# given cookie is valid for up to $::session_timeout * 2 (default 20 minutes).
# Note: since the token also gets updated on iRule modification (in the
# RULE_INIT event), rapid changes to the iRule could cause some open sessions to
# go through the javascript process again as though they were new clients (this
# is what happens if cookie validation fails).
# Having a delay (particularly one that is randomized) is intended to help
# spread out the traffic, but it's not necessary to serve the basic need of
# requiring that an encrypted token be presented from client-side javascript, so
# this functionality could be removed. Also, the way in which the delay is
# achieved is by having javascript that executes a busy loop until enough time
# has passed. This busy loop is likely to cause the client machine's CPU usage
# to spike during the delay. This isn't ideal, however, it would only happen
# during an attack, and even then only once per session for valid clients (i.e.
# this seems like a pretty small price to pay if your site is under attack).
rule verify_with_js_cookie {
when RULE_INIT {
# Settings
set ::maxcpu 60
set ::session_timeout 600
set ::basedelay 5000
set ::cookie "bigverify"
set ::secret "doesntmatter"
# No configurable parameters beyond this point
# If this rule already exists and is modified, act like a normal re-key
# event: update the current key and preserve the previous token
if { [info exists ::key] and [string length $::key] } {
set ::prev_token [b64encode [AES::encrypt $::key $::secret]]
}
set ::key [AES::key 128]
set ::token [b64encode [AES::encrypt $::key $::secret]]
set ::last_rekey [clock seconds]
}
when HTTP_REQUEST {
# By default do not engage the verification mechanism
set need_cookie 0
# If enough time has passed, update the current session token.
# Note: It could be reasonable to move this logic until after need_cookie
# is checked, but in the case where usage is fluctuating near the
# threshold it might prevent re-keying on a regular schedule, thereby
# making it possible to use the same token for longer than intended.
set now [clock seconds]
if { [expr $now - $::last_rekey] > $::session_timeout } {
#log local0. "updating session token"
set ::key [AES::key 128]
set ::prev_token $::token
set ::token [b64encode [AES::encrypt $::key $::secret]]
set ::last_rekey $now
}
# See if we need to bother with verifying the client or not
if { [cpu usage 5sec] > $::maxcpu } {
#log local0. "need cookie"
# This value is used by the HTTP_RESPONSE event to decide whether
# to insert a fresh cookie as part of the server response (which
# only applies if the client already has a valid cookie)
set need_cookie 1
} else {
#log local0. "don't need cookie"
# If the client already has a cookie then we might be fluctuating
# near the threshold, so continue giving the client fresh cookies
if { [HTTP::cookie exists $::cookie] } {
#log local0. "providing cookie anyway"
set need_cookie 1
}
return
}
# If we get here, we need to verify the client cookie (if cookie exists)
# or send them javascript to create a cookie (if cookie doesn't exist or
# is invalid)
# Check for/verify client cookie
if { [HTTP::cookie exists $::cookie] } {
#log local0. "has a cookie"
# Check if clients cookie matches current or previous token
if { ([HTTP::cookie value $::cookie] equals $::token) or
([HTTP::cookie value $::cookie] equals $::prev_token) } {
#log local0. "verification succeeded"
return
}
#log local0. "verification failed"
}
# Generate appropriate redir location whether or not the client sent
# an HTTP Host header
if { [string length [HTTP::host]] } {
set location [HTTP::host][HTTP::uri]
} else {
set location [IP::local_addr][HTTP::uri]
}
# Send client a page with javascript that sleeps, sets a cookie, and
# redirects back here again.
#log local0. "responding with javascript"
HTTP::respond 200 content "
<html><head>
< script type=\"text/javascript\">
function redirect() {
window.location=\"http://$location\";
}
</script>
</head>
<body onload=\"redirect()\">
< script type=\"text/javascript\">
var end=Math.ceil($::basedelay*Math.random()) + $::basedelay;
var start=new Date().getTime();
for (var i=0; i < 1e7; i++) {
if ((new Date().getTime() - start) > end) {
break;
}
}
document.cookie = \"$::cookie=$::token; path=/\";
</script>
</body>
</html>
" "Content-Type" "text/html"
# end of HTTP_REQUEST event
}
when HTTP_RESPONSE {
# If we need to validate cookies or if we the client already had a cookie
# then insert a cookie with the latest token into the server response.
# This is needed to ensure that already-validated clients stay validated
# and don't have to go through the javascript process again.
if { $need_cookie == 1 } {
#log local0. "inserting cookie in response"
HTTP::cookie insert name $::cookie value $::token path "/"
}
}
}
The BIG-IP API Reference documentation contains community-contributed content. F5 does not monitor or control community code contributions. We make no guarantees or warranties regarding the available code, and it may contain errors, defects, bugs, inaccuracies, or security vulnerabilities. Your access to and use of any code available in the BIG-IP API reference guides is solely at your own risk.