Credit Card Scrubber Using a Stream Profile

Problem this snippet solves:

This iRule illustrates how to scrub out Credit Card Numbers from HTTP traffic using a STREAM profile.

This example uses similar logic to scrub out credit card numbers as the CreditCardScrubber rule. Instead of collecting the HTTP response payloads, it uses the stream filter to replace the strings inline. This should be more efficient than buffering the payload with the HTTP::collect command.

There is a list of file extensions defined in RULE_INIT. This allows the administrator to restrict the responses to check for credit card numbers in.

It requires a blank stream profile and a custom HTTP profile with Chunking set to Rechunk if you change the response content length. If you wanted to avoid rechunking responses, you could dynamically replace the found credit card number string with as many X's as the length of the found string in the STREAM_MATCHED event using the STREAM::replace command.

Code :

# v10+ iRule Source

# Updated for 10.x and higher
when RULE_INIT {

   # This regex defines what strings are considered a credit card.  Wrap the regex in curly braces.
   set static::cc_regex {(?: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}}

   # Replace the matched strings with this string.  It can be blank to remove the string altogether.
   # Note, if you wanted to avoid rechunking responses, you could dynamically replace the found credit card 
   # number string with as many X's as the length of the found string in the STREAM_MATCHED event.
   set static::replacement_text "xxxxxxxxxxxxxxxx"

   # As an example, this is a way to limit which requests to check the responses from.
   # This could be replaced with a string datagroup
   set static::uris_to_check_response [list \
      .aspx \
      .asp \
      .html \  
   ]

   # Log debug to /var/log/ltm?  1=yes, 0=no.
   set static::cc_replace_debug 1

   # Disable the stream filter by default
   STREAM::disable
}
when HTTP_REQUEST {

   # Check if request is for a configured file type
   if {[matchclass [string tolower [HTTP::path]] ends_with $static::uris_to_check_response]}{

      set check_response 1
      if {$static::cc_replace_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Request file type matched.  Checking response."}

      # Don't allow response data to be chunked
      if { [HTTP::version] eq "1.1" } {
          if { [HTTP::header is_keepalive] } {
             HTTP::header replace "Connection" "Keep-Alive"
          }
          HTTP::version "1.0"
       }
   
   } else {
      # Don't check responses by default
      if {$static::cc_replace_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Request file type didn't match.  Not checking response."}
      set check_response 0
   }
}
when HTTP_RESPONSE {

   # Check the response if the response we want to check.  You can check all text responses, and/or based on the request type
   if {$check_response && [HTTP::header value Content-Type] contains "text"}{

      if {$static::cc_replace_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Matched Content-Type check."}

      # Set the find/replace strings
      STREAM::expression "@$static::cc_regex@$static::replacement_text@"

      if {$static::cc_replace_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Current stream expression: @$static::cc_regex@$static::replacement_text@"}

      # Enable the stream filter for this response
      STREAM::enable
   }   
}
# STREAM_MATCHED is triggered when the stream filter's find string is found
when STREAM_MATCHED {

   # Log the string which matched the stream profile
   if {$static::cc_replace_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Matched: [STREAM::match]"}
}


# v9.x iRule Source

when RULE_INIT {

   # This regex defines what strings are considered a credit card.  Wrap the regex in curly braces.
   set ::cc_regex {(?: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}}

   # Replace the matched strings with this string.  It can be blank to remove the string altogether.
   # Note, if you wanted to avoid rechunking responses, you could dynamically replace the found credit card 
   # number string with as many X's as the length of the found string in the STREAM_MATCHED event.
   set ::replacement_text "xxxxxxxxxxxxxxxx"

   # As an example, this is a way to limit which requests to check the responses from.  
   set ::uris_to_check_response [list \
      .aspx \
      .asp \
      .html \  
   ]

   # Log debug to /var/log/ltm?  1=yes, 0=no.
   set ::cc_replace_debug 1
}
when HTTP_REQUEST {

   # Disable the stream filter by default
   STREAM::disable

   # Check if request is for a configured file type
   if {[matchclass [string tolower [HTTP::path]] ends_with $::uris_to_check_response]}{

      set check_response 1
      log local0. "[IP::client_addr]:[TCP::client_port]: Request file type matched.  Checking response."

      # Don't allow response data to be chunked
      if { [HTTP::version] eq "1.1" } {
          if { [HTTP::header is_keepalive] } {
             HTTP::header replace "Connection" "Keep-Alive"
          }
          HTTP::version "1.0"
       }
   
   } else {
      # Don't check responses by default
      log local0. "[IP::client_addr]:[TCP::client_port]: Request file type didn't match.  Not checking response."
      set check_response 0

   }
}
when HTTP_RESPONSE {

   # Check the response if the response we want to check.  You can check all text responses, and/or based on the request type
   if {[HTTP::header value Content-Type] contains "text" and $check_response}{

      log local0. "[IP::client_addr]:[TCP::client_port]: Matched Content-Type check."

      # Don't apply the stream profile against 4+Mb response sizes or TMM will restart (reference: SOL6741 / CR70146)
      # You can remove this check if your version has a fix for this issue.
      if {[HTTP::header exists Content-Length] and [HTTP::header value Content-Length] < 4194304}{

         # Wildcard match
         set stream_expression "@$::cc_regex@$::replacement_text@"

         # Set the find/replace strings
         STREAM::expression $stream_expression

         if {$::cc_replace_debug}{  
               log local0. "[IP::client_addr]:[TCP::client_port]: Current stream expression: $stream_expression"
         }  
         # Enable the stream filter for this response
         STREAM::enable
      }
   }   
}
# STREAM_MATCHED is triggered when the stream filter's find string is found
when STREAM_MATCHED {

   # Log the string which matched the stream profile
   if {$::cc_replace_debug}{   
      log local0. "[IP::client_addr]:[TCP::client_port]: Matched: [STREAM::match]"
   }
}
Published Mar 17, 2015
Version 1.0

Was this article helpful?

No CommentsBe the first to comment