cancel
Showing results for 
Search instead for 
Did you mean: 

Problem this snippet solves:

This iRule illustrates how to scrub out Credit Card Numbers from HTTP traffic.

Let's say you want to specify a policy to not allow any credit card numbers outside of your network. How would you go about scrubbing out Credit Card Numbers? This isn't as simple as searching for a string pattern. CCNs vary in length depending on the issuer of the card. But one thing is common: they all must pass the Luhn Formula. Info on the Luhn Formula or MOD 10 can be found here. An excellent reference on credit card number makeup (beyond the 5 types checked in this iRule) is available here.

How to use this snippet:

This rule will match Diners (13 digit), Amex (15 digit), Visa (13 and 16 digit) Mastercard (16 Digit) and Discover (16 Digit).

This example will look matching patterns looking like credit cards and return their indexes into the payload. Then the number is run through the Luhn formula (with optimizations by unRuleY). If it is indeed a valid credit card number, it is masked with X's.

Further modifications added support for CCNs with - or a blank between the numbers. i.e. xxxx-xxxx-xxxx-xxxx, xxxx xxxx xxxx xxxx, xxxxxxxxxxxxxxxx, would match.

To mask all but the last N digits with X's do the following. At the bottom of the script, you see this line:

HTTP::payload replace $card_start $card_len [string repeat "X" $card_len]

Add a small line before it like this: Here N=4.

set card_len [expr {$card_len - 4}]

I used the number 4 to replace all but the last 4 digits. Change this to the number of digits you want to leave untouched. so you end up with:

set card_len [expr {$card_len - 4}]
HTTP::payload replace $card_start $card_len [string repeat "X" $card_len]

Note for an alternate method of implementing this iRule using the stream profile, check the Codeshare example.

Code :

when HTTP_REQUEST {

   # Prevent the server from sending a compressed response
   # remove the compression offerings from the client
   HTTP::header remove "Accept-Encoding"

   # Don't allow response data to be chunked
   if { [HTTP::version] eq "1.1" } {

      # Force downgrade to HTTP 1.0, but still allow keep-alive connections.
      # Since HTTP 1.1 is keep-alive by default, and 1.0 isn't,
      # we need make sure the headers reflect the keep-alive status.

      # Check if this is a keep alive connection
      if { [HTTP::header is_keepalive] } {

         # Replace the connection header value with "Keep-Alive"
         HTTP::header replace "Connection" "Keep-Alive"
      }

      # Set server side request version to 1.0
      # This forces the server to respond without chunking
      HTTP::version "1.0"
   }
}
when HTTP_RESPONSE {

  # Only check responses that are a text content type (text/html, text/xml, text/plain, etc).
  if { [HTTP::header "Content-Type"] starts_with "text/" } {

    # Get the content length so we can collect the data (to be processed in the HTTP_RESPONSE_DATA event)
    # Limit collection to 1Mb (1048576 minus a little to spare) - See SOL6578 for details
    if { [HTTP::header exists "Content-Length"] } {
      if { [HTTP::header "Content-Length"] > 1048000 }{
        # Content-Length over 1Mb so collect 1Mb
        set content_length 1048000
      } else {
        # Content-Length under 1Mb so collect actual length
        set content_length [HTTP::header "Content-Length"]
      }
    } else {
      # Response did not have Content-Length header, so use default of 1Mb
      set content_length 1048000
    }
    # Don't collect content if Content-Length header value was 0
    if { $content_length > 0 } {
       HTTP::collect $content_length
    }
  }
}
when HTTP_RESPONSE_DATA {
  # Find ALL the possible credit card numbers in one pass  
  set card_indices [regexp -all -inline -indices\
    {(?:3[4|7]\d{2})(?:[ ,-]?(?:\d{5}(?:\d{1})?)){2}|(?:4\d{3})(?:[ ,-]?(?:\d{4})){3}|(?:5[1-5]\d{2})(?:[ ,-]?(?:\d{4})){3}|(?:6011)(?:[ ,-]?(?:\d{4})){3}}\
    [HTTP::payload]]

  foreach card_idx $card_indices {
    set card_start [lindex $card_idx 0]
    set card_end [lindex $card_idx 1]
    set card_len [expr {$card_end - $card_start + 1}]
    set card_number [string range [HTTP::payload] $card_start $card_end]
    # Remove dash or space if they exist and count the occurrences in variable cutouts.
    set cutouts [regsub -all {[- ]} $card_number "" card_number]
    # Adjsut card_len variable but keep it for later use.
    set new_card_len [expr {$card_len - $cutouts}]

    set double [expr {$new_card_len & 1}]  
    set chksum 0  
    set isCard invalid

    # Calculate MOD10
    for { set i 0 } { $i < $new_card_len } { incr i } { 
       set c [string index $card_number $i]  
       if {($i & 1) == $double} {  
          if {[incr c $c] >= 10} {incr c -9}  
       }  
       incr chksum $c  
    }  

    # Determine Card Type
    switch [string index $card_number 0] {  
       3 { set type AmericanExpress }  
       4 { set type Visa }  
       5 { set type MasterCard }  
       6 { set type Discover }  
       default { set type Unknown }  
    }
    
    # If valid card number, then mask out numbers with X's  
    if { ($chksum % 10) == 0 } {  
       set isCard valid 
       HTTP::payload replace $card_start $card_len [string repeat "X" $card_len]
    }
    
    # Log Results
    log local0. "Found $isCard $type CC# $card_number"  
  }
}
Version history
Last update:
‎17-Mar-2015 12:27
Updated by:
Contributors