HTTP delay and validate clients using javascript cookies when CPU overloaded

Problem this snippet solves:

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 simply recording a single valid token 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).

Code :

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 "

    < script type=\"text/javascript\">
        function redirect() {
            window.location=\"http://$location\";
        }
    </script>


    < 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>


" "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 "/"
    }
}
}
Published Mar 18, 2015
Version 1.0

Was this article helpful?

1 Comment

  • Had to change the response because of errors:

    <!DOCTYPE html>
    <html >
    <head >
        <script >
            function redirect() {
                window.location=\"http://$location\";
            }
        </script >
    </head >
    <body onload=\"redirect()\" >
        <script >
            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"

    Mostly adding space before the > sign