Learn F5 Technologies, Get Answers & Share Community Solutions Join DevCentral

Filter by:
  • Solution
  • Technology
Answers

iRule Optimization

Hi Folks,

a customer is using an iRule, which logs full POST requests, including the passwords of a login in cleartext. He asked me to masquerade the password. Unfortunately I didn't found an easy way to do this and therefore written the following snippet of code.

when HTTP_REQUEST {

    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%&cu)eP,ssword"

    log local0. "Request: $data"
    set user_password_match "&password="
    set user_password_match_len [string length $user_password_match]
    set user_password_pos [expr {[string last $user_password_match $data]+$user_password_match_len}]
    set user_password [string range $data $user_password_pos end]
    log local0. "User password is: $user_password"

    # Only necessary to have the same length of asterisk as chars in the origion password
    set user_password_list [split $user_password ""]
    set user_password_new ""
    foreach pwchar $user_password_list {
        append user_password_new "*"
    }

    log local0. "Masked password is: $user_password_new"

    set data_new [string map "$user_password $user_password_new" $data]

    log local0. "Request (masked): $data_new"
}

Of course the logging is testing purposes during the development. But to be honest, I don't believe that this is the most easiest and efficient way to reach my goal. Does anybody have an idea how to optimize this iRule and make it more efficient?

Thanks in advance.

Greets, svs

0
Rate this Question
Comments on this Question
Comment made 16-Feb-2017 by svs 294

This is an Example Output of the iRule:

<HTTP_REQUEST>: Request: POST /some_login HTTP/1.1  Host: login.example.com  Accept: */*  Content-length: 65  Content-Type: application/x-www-form-urlencoded    username=some_user&password=Thi`isMyHig!S%&cu)eP,ssword
<HTTP_REQUEST>: User password is: Thi`isMyHig!S%&cu)eP,ssword
<HTTP_REQUEST>: Masked password is: ***************************
<HTTP_REQUEST>: Request (masked): POST /some_login HTTP/1.1  Host: login.example.com  Accept: */*  Content-length: 65  Content-Type: application/x-www-form-urlencoded    username=some_user&password=***************************
0

Answers to this Question

placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Hi svs,

you may try the iRule below. The password masking code function should run ~ 3-times faster then yours and also contains extended error handlings.

when RULE_INIT {
    set static::user_password_match "&password="
    set static::user_password_match_len [string length $static::user_password_match]    
}
when HTTP_REQUEST {
    # define input data
    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%xcu)eP,ssword&parmX=1&parmY=2&parmZ=3"
    # parse input data
    set data_new [substr $data 0 $static::user_password_match]
    if { [string length $data] != [string length $data_new] } then {
        set data_remain [findstr $data $static::user_password_match $static::user_password_match_len]
        if { $data_remain contains "&" } then {
            append data_new "$static::user_password_match[string repeat "*" [string length [substr $data_remain 0 "&"]]][findstr $data_remain "&" 0]"
        } else {
            append data_new "$static::user_password_match[string repeat "*" [string length $data_remain]]"
        }
    }
    # debug log 
    log local0.debug "Request: $data"
    log local0.debug "User password is: [substr $data_remain 0 "&"]"
    log local0.debug "Masked password is: [string repeat "*" [string length [substr $data_remain 0 "&"]]]"
    log local0.debug "Request (masked): $data_new"
}

Cheers, Kai

0
Comments on this Answer
Comment made 17-Feb-2017 by Kai Wilke 6138

Hi Svs,

based on Jie's post, you may also try the iRule below...

when HTTP_REQUEST {
    # define input data
    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%xcu)eP,ssword&parmX=1&parmY=2&parmZ=3"
    # parse input data
    set data_new ""
    foreach element [split $data "&"] {
        if { $element starts_with "password" } then {
            lappend data_new "password=[string repeat "*" [string length [findstr $element "=" 1]]]"
        } else {
            lappend data_new $element
        }
    }
    set data_new [join $data_new "&"]
    # debug log 
    log local0.debug "Request: $data"
    log local0.debug "Request (masked): $data_new"
}

... its much less complicated to read, but on the other hand slightly slower then my previous code example.

Cheers, Kai

0
Comment made 01-Mar-2017 by svs 294

Thanks for the responses!

Sorry for delaying a reaction on this. I checked out the timing feature and played around a bit on how to get meaningful statistics.

@Kai: You optimization seems to be very optimized. I will check this out asap and let you know the timing results.

Greets, svs

0
Comment made 01-Mar-2017 by svs 294

I've done a short test.

My iRule adds 0.0834584 msecs to processing, while the optimized one from Kai add 0.0708751 msecs. This is a difference of 0.0125833 msecs. I can't really assess how big this difference would be under pressure.

I've used the calculation method described by unRuleY here: https://devcentral.f5.com/questions/Timing-iRules

For the test I've done just one request per iRule.

However, I will take your iRule, as it is less time consuming and makes use of functions, which I wasn't aware of while writing the code (i.e. string repeat).

Thank you very much!!

0
Comment made 07-Mar-2017 by Kai Wilke 6138

Hi Svs,

I've used a homegrown method (check out) to measure the performance of the individual iRules snippets. The benefit of using the homegrown method is an included %cutoff calculation to rule out statistical outliers. Beside of that, I've also removed all the debugging [log] statements before running the tests, to reflect only the differences of the individual parsing functions.

When performing the same tests using the build-in timing on method, you will get these results...

Your original iRule:

when HTTP_REQUEST timing on {

    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%xcu)eP,ssword"


    set user_password_match "&password="
    set user_password_match_len [string length $user_password_match]
    set user_password_pos [expr {[string last $user_password_match $data]+$user_password_match_len}]
    set user_password [string range $data $user_password_pos end]


    # Only necessary to have the same length of asterisk as chars in the origion password
    set user_password_list [split $user_password ""]
    set user_password_new ""
    foreach pwchar $user_password_list {
        append user_password_new "*"
    }

    set data_new [string map "$user_password $user_password_new" $data]

    # log local0. "Request: $data"
    # log local0. "Masked password is: $user_password_new"
    # log local0. "User password is: $user_password"
    # log local0. "Request (masked): $data_new"

}

Result:

--------------------------------------------
Ltm::Rule Event: iRule_2_Delete:HTTP_REQUEST
--------------------------------------------
Priority                    500
Executions             
  Total                     500
  Failures                    0
  Aborts                      0
CPU Cycles on Executing
  Average                141.6K
  Maximum                190.0K
  Minimum                     0

Jie's approach:

when HTTP_REQUEST timing on {

    # define input data
    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%xcu)eP,ssword&parmX=1&parmY=2&parmZ=3"
    # parse input data
    set data_new ""
    foreach element [split $data "&"] {
        if { $element starts_with "password" } then {
            lappend data_new "password=[string repeat "*" [string length [findstr $element "=" 1]]]"
        } else {
            lappend data_new $element
        }
    }
    set data_new [join $data_new "&"]
    # debug log 
    #log local0.debug "Request: $data"
    #log local0.debug "Request (masked): $data_new"

}

Result:

--------------------------------------------
Ltm::Rule Event: iRule_2_Delete:HTTP_REQUEST
--------------------------------------------
Priority                    500
Executions             
  Total                     500
  Failures                    0
  Aborts                      0
CPU Cycles on Executing
  Average                 83.4K
  Maximum                199.3K
  Minimum                     0

My approach:

when RULE_INIT {
    set static::user_password_match "&password="
    set static::user_password_match_len [string length $static::user_password_match]    
}
when HTTP_REQUEST timing on {

    # define input data
    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%xcu)eP,ssword&parmX=1&parmY=2&parmZ=3"
    # parse input data
    set data_new [substr $data 0 $static::user_password_match]
    if { [string length $data] != [string length $data_new] } then {
        set data_remain [findstr $data $static::user_password_match $static::user_password_match_len]
        if { $data_remain contains "&" } then {
            append data_new "$static::user_password_match[string repeat "*" [string length [substr $data_remain 0 "&"]]][findstr $data_remain "&" 0]"
        } else {
            append data_new "$static::user_password_match[string repeat "*" [string length $data_remain]]"
        }
    }
    # debug log 
    # log local0.debug "Request: $data"
    # log local0.debug "User password is: [substr $data_remain 0 "&"]"
    # log local0.debug "Masked password is: [string repeat "*" [string length [substr $data_remain 0 "&"]]]"
    # log local0.debug "Request (masked): $data_new"
}

Result:

--------------------------------------------
Ltm::Rule Event: iRule_2_Delete:HTTP_REQUEST
--------------------------------------------
Priority                    500
Executions             
  Total                     500
  Failures                    0
  Aborts                      0
CPU Cycles on Executing
  Average                 61.7K
  Maximum                111.2K
  Minimum                     0

Note: Using the build-in timing on tests, the differences between these code blocks are still a factor of ~2,5

Cheers, Kai

1
Comment made 08-Mar-2017 by svs 294

Hi Kai,

this is awesome! Thank you very much!

Double-"Accepted answer" ;-)

Greets, svs

0
Comment made 08-Mar-2017 by Kai Wilke 6138

Double-"You're welcome" ;-)

Cheers, Kai

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER
Comments on this Answer
Comment made 16-Feb-2017 by svs 294

Hi Jie,

thanks for your response.

I can't see why this iRule may be more efficient. This would result in an array of POST parameters, but I would need to loop through it, which may be more resource intensive then my way. Am I wrong? Furthermore, at the the point where I need to implement this code, only $data is available, which is filled within another event.

Greets, svs

0
Comment made 16-Feb-2017 by Jie 2107

Well, you can turn on timing for comparison, e.g.:

when HTTP_REQUEST timing on {
    set data "POST /some_login HTTP/1.1\r\nHost: login.example.com\r\nAccept: */*\r\nContent-length: 65\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=some_user&password=Thi`isMyHig!S%&cu)eP,ssword"
    foreach x [split $data "&"] {
        if { $x starts_with "password=" } {
            log local0. "password=********"
        } else {
            log local0. "$x"
        }
    }
}
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Hi,

I have some comments about your irule:

POST data are not included in HTTP_REQUEST but in HTTP_REQUEST_DATA. when you will have to log these informations, there will be:

  • [HTTP::request] in HTTP_REQUEST event
  • [HTTP::payload] in HTTP_REQUEST_DATA event

if you want to parse only POST data, you can use this irule::

when HTTP_REQUEST {
    # Collect Post Data to be parsed in HTTP_REQUEST_DATA
    if { [HTTP::method] eq "POST" }{
        set clength 0
        if {[HTTP::header exists "Content-Length"] && [HTTP::header Content-Length] <= 1048576}{
          set clength [HTTP::header Content-Length]
        } else { set clength 1048576 }
        if { [info exists clength] && $clength > 0} { HTTP::collect $clength }
    }   
}
when HTTP_REQUEST_DATA {
    set payload [HTTP::payload]
    # Convert POST Data to URI with query string to extract password parameter
    set password  [URI::query "/?$payload" password]
    # Replace Post 
    set new_data [string map "password=$password password=[string repeat "*" [string length $password]]"  $payload]
}
0
Comments on this Answer
Comment made 02-Mar-2017 by svs 294

Hi,

basically you're right. This example iRule doesn't take POST data from a real request. The real iRule is much more complicated. The POST request is created manually for a sideband connection. During this process there are some debug log messages, which logs the clear-text password. I've just taken out the code which I wanted to use for password masking.

But thanks for your objection and clarification.

Greets, svs

0