Forum Discussion
Joel_66443
Nimbostratus
Jun 16, 2009Encrypting 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
- hoolio
Cirrostratus
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 - Joel_66443
Nimbostratus
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. - hoolio
Cirrostratus
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 - hoolio
Cirrostratus
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¶m4=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¶m4=value4
: 2.2.2.2:1350: CCV updated payload (172 bytes): param1=value1&CCNumber=p%2bdoUqoaAjaxR2FnqqklbiUMNNIabTOGsi0arUlPBLMSmeYkIegNhVTqVhtg6K2z0NgAxX34&CCV=gFf4mRcMyid3G5lnqqklbiUMNNIabWA3gSJ9LT0ePcUKKX2ap6HxMPzl¶m4=value4
Aaron - Joel_66443
Nimbostratus
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! - hoolio
Cirrostratus
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
Help guide the future of your DevCentral Community!
What tools do you use to collaborate? (1min - anonymous)Recent Discussions
Related Content
DevCentral Quicklinks
* Getting Started on DevCentral
* Community Guidelines
* Community Terms of Use / EULA
* Community Ranking Explained
* Community Resources
* Contact the DevCentral Team
* Update MFA on account.f5.com
Discover DevCentral Connects