Forum Discussion

Joel_66443's avatar
Joel_66443
Icon for Nimbostratus rankNimbostratus
Jun 16, 2009

Encrypting POST variables before they hit the server

Hi All,

 

I'm attempting to encrypt two form variables as they are in transit from the user's browser to the web server. That is, I want to encrypt the two variables before they hit the web server.

 

 

The variables in this case consist of a credit card number (CCNumber) and ccv number (CCV) number.

 

 

In order to accomplish this, I am attempting to split the HTTP: Payload into it's key/value pairs, then encrypt the two form values, then finally compile a new HTTP: Payload to replace the old before sending it onto the webserver.

 

 

I'm finding that it's not quite as simple to implement as first thought. whilst the iRule does indeed encrypt the two variables and rebuild a new HTTP: Payload, it fails to replace the old with the new, balking with the error:

 

 

 - Out of bounds (line 1)     invoked from within "HTTP::payload replace 0 [string length $::newPayload] $::newPayload"

 

 

Attempts to place the 'HTTP::payload replace' into HTTP_RESPONSE_DATA fails to modify the POST data in any way. It's still unencrypted to the server.

 

 

I'd sincerely appreciate if a more experienced engineer could have a glance over the code and pick out any key errors.

 

- Jason

 

 

  
 when RULE_INIT { 
    set ::aes_key 7ef82a9b038c19d4a6b2c014d812 
 } 
  
 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" 
  
 log local0. "Original Payload:  [HTTP::payload]" 
 log local0. "Original Payload Length:  [string length [HTTP::payload]]" 
  
       set parametersList [split [HTTP::payload] "&"] 
          for {set i 0} {$i < [llength $parametersList]} {incr i} { 
             set parameter [split [lindex $parametersList $i] "="] 
             set ::postVars([lindex  $parameter 0]) [lindex  $parameter 1]; 
          } 
  
        encrypt the credit card number and ccv 
       set card_number_encrypted [URI::encode [b64encode [AES::encrypt $::aes_key $::postVars(CCNumber)]]] 
       set ccv_number_encrypted [URI::encode [b64encode [AES::encrypt $::aes_key $::postVars(CCV)]]] 
  
        create a new http payload 
       set ::newPayload "CCNumber=$card_number_encrypted&CCV=$ccv_number_encrypted" 
  
 log local0. "New Payload: $::newPayload" 
 log local0. "New Payload Length: [string length $::newPayload]" 
  
        replace the old payload with the new 
       HTTP::payload replace 0 [string length $::newPayload] $::newPayload 
    } 
 } 
  
 when HTTP_RESPONSE { 
 } 
  
 when HTTP_RESPONSE_DATA { 
       HTTP::payload replace 0 [string length $::newPayload] $::newPayload 
 }  
 

 

 

 

The following logs demonstrate that the script is successfully compiling a new payload, but is failing to apply it to the stream.

 

 
 Original Payload:  CCNumber=1111+1111+1111+1111&CCV=123  
 Original Payload Length:  53  
 New Payload: CCNumber=edCEWkxt0jh28CkaNezCLift0eHxpJRGiNLMXoBk8RU5AnBMo9xzaubcwspj%2bLLqZXJfDsaUl9ij&CCV=OaszTOZA%2by5P2UAaNezCLift0eHxpHggtCfx8%2fSs4q9uNtTGxEthMTA%3d  
 New Payload Length: 171  
 

6 Replies

  • Hi,

     

     

    There are a few issues with the example you posted. I'll provide some suggestions on how to do this in a bit, but first I'm wondering why you would encrypt just portions of this data only between LTM and the server(s). Can you explain the use case for this?

     

     

    If you're sending credit card numbers over an untrusted network it should be done with the entire connection encrypted. It would be more efficient in terms of LTM resources to encrypt the connection versus individual components of the request. It would also eliminate the need for the application to decode and decrypt the parameters.

     

     

    Aaron

     

  • True, in most situations it would be prudent to encrypt the entire connection rather than a few elements of the message sent from the client. My case is different however as I am designing a system for PCI compliance which will encrypt credit card values as they're entering the system, and then decrypt them as they're leaving the system.
  • That's an interesting concept.

     

     

    You have some of the pieces in your example, but there are a few issues. Some or all of the payload may be available in HTTP_REQUEST using the HTTP::payload command, but it will only be what the client sent in the first packet. To ensure the full payload is collected, you should trigger HTTP payload collection using HTTP::collect. You can then replace the payload in the request event, HTTP_REQUEST_DATA, in order to rewrite the payload before the request is sent to the pool. The HTTP::payload replace command expects the original content length, so you can use 'HTTP::payload replace 0 [HTTP::payload length] $new_payload'.

     

     

    Be careful with global variables. Any variable declared in RULE_INIT, with the global command or prefixed with :: will be global. Global variables are accessible (and changeable) on any TCP connection from any client. Your ::postVars array should be local to ensure the values aren't trampled by other clients. However, instead of looping through the parameter names and values, you can use a workaround with URI::query (Click here) to get the parameter values.

     

     

     
     when RULE_INIT { 
      
         Hardcode an encryption key 
        set ::aes_key 7ef82a9b038c19d4a6b2c014d812  
      
         Credit Card parameter name 
        set ::cc_param_name "CCNumber" 
      
         Card Code Verification parameter name 
        set ::ccv_param_name "CCV" 
      
         Maximum collection size (in bytes) 
        set ::max_collect_size 1000000 
      
         Log debug messages? 1=yes, 0=no. 
        set ::cc_encrypt_debug 1 
     } 
     when HTTP_REQUEST { 
      
         Check if request is a POST 
        if {[HTTP::method] eq "POST"}{ 
      
            If we're logging debug messages, save the client IP:port to make log lines shorter. 
           if {$::cc_encrypt_debug}{ 
              set log_prefix "[IP::client_addr]:[TCP::client_port]" 
              log local0. "$log_prefix: New POST request to [HTTP::host][HTTP::uri]" 
           } 
            Trigger collection for up to 1MB of data 
           if {([HTTP::header exists "Content-Length"]) && ([HTTP::header "Content-Length"] <= $::max_collect_size)}{ 
              set content_length [HTTP::header "Content-Length"] 
           } else { 
              set content_length $::max_collect_size 
           } 
           if { [info exists content_length] && $content_length > 0} { 
              HTTP::collect $content_length 
              if {$::cc_encrypt_debug}{log local0. "$log_prefix: Collecting $content_length bytes"} 
           } 
        } 
     } 
     when HTTP_REQUEST_DATA { 
      
        if {$::cc_encrypt_debug}{log local0. "$log_prefix: Original payload ([HTTP::payload length] bytes): [HTTP::payload]"} 
      
         Parse the Credit Card number and CCV parameter values from the payload using a URI::query 
        set cc_unencrypted [URI::query "?[HTTP::payload]" $::cc_param_name] 
        set ccv_unencrypted [URI::query "?[HTTP::payload]" $::ccv_param_name] 
      
        if {$::cc_encrypt_debug}{log local0. "$log_prefix: Parsed \$cc_unencrypted=$cc_unencrypted, \$ccv_unencrypted=$ccv_unencrypted"} 
      
         Replace the CC with the encrypted version if a CC parameter value was parsed 
        if {[string length $cc_unencrypted]}{ 
      
            Replace the Credit Card number parameter name=value pair with the encrypted and encoded version. 
              We use the full param_nam=value for searching/replacing to reduce the chance of a false match 
           HTTP::payload replace 0 [HTTP::payload length] [regsub -all "${::cc_param_name}=${cc_unencrypted}" [HTTP::payload] "${::cc_param_name}=[URI::encode [b64encode [AES::encrypt $::aes_key $cc_unencrypted]]]"] 
      
           if {$::cc_encrypt_debug}{log local0. "$log_prefix: CC updated payload ([HTTP::payload length] bytes): [HTTP::payload]"} 
        } 
         Replace the CCV with the encrypted version if a CCV parameter value was parsed 
        if {[string length $ccv_unencrypted]}{ 
      
            Replace the Card Code Verification parameter name=value pair with the encrypted and encoded version. 
              We use the full param_nam=value for searching/replacing to reduce the chance of a false match 
           HTTP::payload replace 0 [HTTP::payload length] [regsub -all "${::ccv_param_name}=${ccv_unencrypted}" [HTTP::payload] "${::ccv_param_name}=[URI::encode [b64encode [AES::encrypt $::aes_key $ccv_unencrypted]]]"] 
      
           if {$::cc_encrypt_debug}{log local0. "$log_prefix: CCV updated payload ([HTTP::payload length] bytes): [HTTP::payload]"} 
        } 
     } 
     

     

     

    I haven't test this fully but I think it shows the concepts for collecting the request payload and modifying the content with encrypted values.

     

     

    To decrypt the response content you would want to add in some of the request modifications in your original rule (like removing the Accept-Encoding header, setting the HTTP version to 1.0, and other steps listed in the HTTP::collect example Click here). You would then need to write a regex which matches all the potential forms for the CC and CCV numbers and decrypt them.

     

     

    With all that said, I think it would be more efficient to use a stream profile and stream iRule to encrypt/decrypt the CC and CCV values. The advantage of a stream profile and iRule versus collecting the payload is that there is less buffering of content. You can check the last example on the STREAM::expression wiki page (Click here) for an example which sets a stream expression and then modifies the matching string in the STREAM_MATCHED event. If you give this a shot and get stuck, reply with what you've tried and we can try to help.

     

     

    Aaron
  • Here is some sample log output from a quick test:

     

     

    : 2.2.2.2:1350: New POST request to 1.1.1.1/test.html

     

    : 2.2.2.2:1350: Collecting 62 bytes

     

    : 2.2.2.2:1350: Original payload (62 bytes): param1=value1&CCNumber=1234123412341234&CCV=4321&param4=value4

     

    : 2.2.2.2:1350: Parsed $cc_unencrypted=1234123412341234, $ccv_unencrypted=4321

     

    : 2.2.2.2:1350: CC updated payload (120 bytes): param1=value1&CCNumber=p%2bdoUqoaAjaxR2FnqqklbiUMNNIabTOGsi0arUlPBLMSmeYkIegNhVTqVhtg6K2z0NgAxX34&CCV=4321&param4=value4

     

    : 2.2.2.2:1350: CCV updated payload (172 bytes): param1=value1&CCNumber=p%2bdoUqoaAjaxR2FnqqklbiUMNNIabTOGsi0arUlPBLMSmeYkIegNhVTqVhtg6K2z0NgAxX34&CCV=gFf4mRcMyid3G5lnqqklbiUMNNIabWA3gSJ9LT0ePcUKKX2ap6HxMPzl&param4=value4

     

     

    Aaron
  • Many thanks Aaron. The script works perfectly!

     

     

    I'm a little concerned with the prospect of a client overwriting another client's cc_param_name and ccv_param_name variables for obvious reasons. Is this at all possible with this rule? Would moving to a stream rule resolve this potential issue?

     

    Many thanks for your help!
  • The only issue with potentially overwriting the CC values was if you were using a global variable to store the CC numbers. The example I posted uses local variables which are specific to one TCP connection, so it wouldn't be an issue.

     

     

    The stream option would be more efficient, but not any more secure.

     

     

    Aaron