Author Topic: PCI DSS - Make Virtuemart Compliant Code  (Read 19758 times)

chris.t.uk

  • Contributing Developer
  • Jr. Member
  • *
  • Posts: 98
PCI DSS - Make Virtuemart Compliant Code
« on: January 15, 2010, 12:01:00 pm »
I assume if you are capturing the customer credit card details then they will be processed offline, and so you would then have a paper copy of the customers full credit card details (therefore not requiring the storage on server), for correlation purposes this code stores the last four digits of the card number only! We should not store the CVV/CVS/CVV2 longer than required for processing so this is updated to '000' once complete.  The card expiration date is, I believe, OK to store alongside this now fungled data.

This code runs on update order status so as Virtuemart classes a credit card captured sale as 'Pending' it will update all credit card payments that are not in Pending (deal with old numbers).

So, if you are hacked the worst case scenario is that you will lose a few current customer credit card details but not lots and lots of archived data. (hopefully reducing your fines from Visa/MasterCard)

File: administrator/components/com_virtuemart/classes/ps_order.php
In function order_status_update(&$d)
At end just before ‘return true;’ so it only runs on a success!
Add the bold text:
           //echo "RUNNING PCI COMPLIANCE";
      //make sure we are PCI DSS Compliant, eradicate customer credit card codes that have been processed.
      $this->pcidss_compliance();


      return true;
   }


Then add in a new function (just below the existing } from above)
Code: [Select]
function pcidss_compliance(){

/* For PCI compliance we may only store the last four digits and the CVV must not be stored, setting to general 000,
convert the credit card number to ############1234 (keep last four digits for future discussions with customer,
'the card ends in.....'
*/

$q = "SELECT `#__{vm}_order_payment`.`order_id`, order_payment_code,
".VM_DECRYPT_FUNCTION."(order_payment_number,'".ENCODE_KEY."') AS creditCardNumber,
CHAR_LENGTH(".VM_DECRYPT_FUNCTION."(order_payment_number,'".ENCODE_KEY."')) AS creditCardLength
FROM
`#__{vm}_order_payment` LEFT JOIN `#__{vm}_orders` ON
  `#__{vm}_order_payment`.`order_id` = `#__{vm}_orders`.`order_id`
WHERE
order_status != 'P'
AND CHAR_LENGTH(".VM_DECRYPT_FUNCTION."( order_payment_number, '".ENCODE_KEY."' )) > 8
AND LEFT( ".VM_DECRYPT_FUNCTION."( order_payment_number, '".ENCODE_KEY."' ), 8 ) != '########'";

$db = new ps_DB;
$dbUpdate = new ps_DB;

$mask = '##########################'; //24 mask characters, most cards are 16 but some are 17/18 so built in extra

$db->query( $q );

// Now update each ordered product
while( $db->next_record() ) {

//print "UPDATING ".$db->f("order_id");

$q = "UPDATE #__{vm}_order_payment SET
order_payment_code = '000', order_payment_number =
".
VM_ENCRYPT_FUNCTION."('".substr($mask, 0, $db->f("creditCardLength") - 4) . substr($db->f("creditCardNumber"), -4)."','".ENCODE_KEY."')
WHERE order_id = ".$db->f("order_id").";";

//echo $q."<br/>";

$dbUpdate->query( $q );
}
}


Regards,

Chris.
Hacks
Export Orders & Batch Update -> http://forum.virtuemart.net/index.php?topic=52215.0
UK Counties -> http://forum.virtuemart.net/index.php?topic=33968.0
Sites running Virtuemart
http://www.ladders247.co.uk http://www.tools247.co.uk http://www.mackay.co.uk
Synchronization
Synchronized with our instore system via PrePrepSQL and missing Image Remote Retrieval system ;-)

mercomarine

  • Jr. Member
  • **
  • Posts: 129
    • Merco Marine
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #1 on: February 23, 2010, 16:51:42 pm »
Could you explain a little further how this would work from a user standpoint. Basically, if I understand correctly, using your code if you change an order status to pending, it re-writes the credit card number? Or is it the other way around, if the order status is anything but pending it rewrites?
Joomla Version 1.5.23
VirtueMart 1.1.9 stable
Apache: 1.3.42
MySQL 5.0.91
PHP: 5.2.5
phpMyAdmin 3.3.10.2

mercomarine

  • Jr. Member
  • **
  • Posts: 129
    • Merco Marine
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #2 on: February 23, 2010, 16:55:43 pm »
What if I have changed my list order status types? I changed the default from "Pending" to say "Received" but the Status code is still "P"?
Joomla Version 1.5.23
VirtueMart 1.1.9 stable
Apache: 1.3.42
MySQL 5.0.91
PHP: 5.2.5
phpMyAdmin 3.3.10.2

mercomarine

  • Jr. Member
  • **
  • Posts: 129
    • Merco Marine
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #3 on: February 23, 2010, 17:30:28 pm »
Well,

I tried it and as soon as I changed the file on the server and tried to view an order I got an internal server error:

Here is my code that I modified per your instructions (I THINK. In the future, please post a before and after example code so people can compare and contrast old and new, otherwise you have no way to check yourself):

Code: [Select]
function order_status_update(&$d) {
global $mosConfig_offset;

$db = new ps_DB;
//$timestamp = time() + ($mosConfig_offset*60*60);  //Original
$timestamp = time();  //Custom
//$mysqlDatetime = date("Y-m-d G:i:s",$timestamp);  //Original
$mysqlDatetime = date("Y-m-d G:i:s", $timestamp + ($mosConfig_offset*60*60));  //Custom

if( empty($_REQUEST['include_comment'])) {
$include_comment="N";
}

// get the current order status
$curr_order_status = @$d["current_order_status"];
$notify_customer = empty($d['notify_customer']) ? "N" : $d['notify_customer'];
if( $notify_customer=="Y" ) {
$notify_customer=1;
}
else {
$notify_customer=0;
}

$d['order_comment'] = empty($d['order_comment']) ? "" : $d['order_comment'];
if( empty($d['order_item_id']) ) {
// When the order is set to "confirmed", we can capture
// the Payment with authorize.net
if( $curr_order_status=="P" && $d["order_status"]=="C") {
$q = "SELECT order_number,payment_class,order_payment_trans_id FROM #__{vm}_payment_method,#__{vm}_order_payment,#__{vm}_orders WHERE ";
$q .= "#__{vm}_order_payment.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_orders.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_order_payment.payment_method_id=#__{vm}_payment_method.payment_method_id";
$db->query( $q );
$db->next_record();
$payment_class = $db->f("payment_class");
$d["order_number"] = $db->f("order_number");

switch( $payment_class ) {
case "ps_authorize":

require_once( CLASSPATH."payment/ps_authorize.cfg.php");
if( AN_TYPE == 'AUTH_ONLY' ) {
require_once( CLASSPATH."payment/ps_authorize.php");
$authorize = new ps_authorize();
if( !$authorize->capture_payment( $d )) {
return false;
}
}
break;
default:
// default case for payment methods that allow to "capture" the payment
if( is_file( CLASSPATH.'payment/'.basename($payment_class) ) ) {
require_once( CLASSPATH.'payment/'.basename($payment_class) );
if( !class_exists($payment_class)) break;
$paymentObj = new $payment_class();
if( !method_exists($paymentObj,'capture_payment')) break;
if( !$paymentObj->capture_payment( $d )) {
return false;
}
}
}
}
/*
* This is like the test above for delayed capture only
* we (well, I - durian) don't think the credit card
* should be captured until the item(s) are shipped.
* In fact, VeriSign says not to capture the cards until
* the item ships.  Maybe this behavior should be a
* configurable item?
*
* When the order changes from Confirmed or Pending to
* Shipped, perform the delayed capture.
*
* Restricted to PayFlow Pro for now.
*/
if( ($curr_order_status=="P" || $curr_order_status=="C") && $d["order_status"]=="S") {
$q = "SELECT order_number,payment_class,order_payment_trans_id FROM #__{vm}_payment_method,#__{vm}_order_payment,#__{vm}_orders WHERE ";
$q .= "#__{vm}_order_payment.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_orders.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_order_payment.payment_method_id=#__{vm}_payment_method.payment_method_id";
$db->query( $q );
$db->next_record();
$payment_class = $db->f("payment_class");
if( $payment_class=="payflow_pro" ) {
require_once( CLASSPATH."payment/payflow_pro.cfg.php");
if( PFP_TYPE == 'A' ) {
require_once( CLASSPATH."payment/payflow_pro.php");
$pfp = new ps_pfp();
$d["order_number"] = $db->f("order_number");
if( !$pfp->capture_payment( $d )) {
return false;
}
}
}
}

/*
* If a pending order gets cancelled, void the authorization.
*
* It might work on captured cards too, if we want to
* void shipped orders.
*
* Restricted to PayFlow Pro for now.
*/
if( $curr_order_status=="P" && $d["order_status"]=="X") {
$q = "SELECT order_number,payment_class,order_payment_trans_id FROM #__{vm}_payment_method,#__{vm}_order_payment,#__{vm}_orders WHERE ";
$q .= "#__{vm}_order_payment.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_orders.order_id='".$db->getEscaped($d['order_id'])."' ";
$q .= "AND #__{vm}_order_payment.payment_method_id=#__{vm}_payment_method.payment_method_id";
$db->query( $q );
$db->next_record();
$payment_class = $db->f("payment_class");
if( $payment_class=="payflow_pro" ) {
require_once( CLASSPATH."payment/payflow_pro.cfg.php");
if( PFP_TYPE == 'A' ) {
require_once( CLASSPATH."payment/payflow_pro.php");
$pfp = new ps_pfp();
$d["order_number"] = $db->f("order_number");
if( !$pfp->void_authorization( $d )) {
return false;
}
}
}
}

$fields =array( 'order_status'=> $d["order_status"],
'mdate'=> $timestamp );
$db->buildQuery('UPDATE', '#__{vm}_orders', $fields, "WHERE order_id='" . $db->getEscaped($d["order_id"]) . "'");
$db->query();

// Update the Order History.
$fields = array( 'order_id' => $d["order_id"],
'order_status_code' => $d["order_status"],
'date_added' => $mysqlDatetime,
'customer_notified' => $notify_customer,
'comments' => $d['order_comment']
);
$db->buildQuery('INSERT', '#__{vm}_order_history', $fields );
$db->query();

// Do we need to re-update the Stock Level?
if( (strtoupper($d["order_status"]) == "X" || strtoupper($d["order_status"])=="R")
// && CHECK_STOCK == '1'
&& $curr_order_status != $d["order_status"]
) {
// Get the order items and update the stock level
// to the number before the order was placed
$q = "SELECT product_id, product_quantity FROM #__{vm}_order_item WHERE order_id='".$db->getEscaped($d["order_id"])."'";
$db->query( $q );
$dbu = new ps_DB;
require_once( CLASSPATH.'ps_product.php');
// Now update each ordered product
while( $db->next_record() ) {
if( ENABLE_DOWNLOADS == '1' && ps_product::is_downloadable($db->f("product_id")) && VM_DOWNLOADABLE_PRODUCTS_KEEP_STOCKLEVEL == '1') {
$q = "UPDATE #__{vm}_product 
SET product_sales=product_sales-".$db->f("product_quantity")."
WHERE product_id=".$db->f("product_id");
$dbu->query( $q );
}
else {
$q = "UPDATE #__{vm}_product
SET product_in_stock=product_in_stock+".$db->f("product_quantity").",
product_sales=product_sales-".$db->f("product_quantity")."
WHERE product_id=".$db->f("product_id");
$dbu->query( $q );
}
}
}
// Update the Order Items' status
$q = "SELECT order_item_id FROM #__{vm}_order_item WHERE order_id=".$db->getEscaped($d['order_id']);
$db->query($q);
$dbu = new ps_DB;
while ($db->next_record()) {
$item_id = $db->f("order_item_id");
$fields =array( 'order_status'=> $d["order_status"],
'mdate'=> $timestamp );
$dbu->buildQuery('UPDATE', '#__{vm}_order_item', $fields, "WHERE order_item_id='" .(int)$item_id . "'");
$dbu->query();
}

if (ENABLE_DOWNLOADS == '1') {
##################
## DOWNLOAD MOD
$this->mail_download_id( $d );
}

if( !empty($notify_customer) ) {
$this->notify_customer( $d );
}
} elseif( !empty($d['order_item_id'])) {
// Update the Order Items' status
$q = "SELECT order_item_id, product_id, product_quantity FROM #__{vm}_order_item
WHERE order_id=".$db->getEscaped($d['order_id'])
. ' AND order_item_id='.intval( $d['order_item_id'] );
$db->query($q);
$item_product_id = $db->f('product_id');
$item_product_quantity = $db->f('product_quantity');

if( ENABLE_DOWNLOADS == '1' && ps_product::is_downloadable($item_product_id) && VM_DOWNLOADABLE_PRODUCTS_KEEP_STOCKLEVEL == '1') {
$q = "UPDATE #__{vm}_product 
SET product_sales=product_sales-".$item_product_quantity."
WHERE product_id=".$item_product_id;
$db->query( $q );
}
else {
$q = "UPDATE #__{vm}_product
SET product_in_stock=product_in_stock+".$item_product_quantity.",
product_sales=product_sales-".$item_product_quantity."
WHERE product_id=".$item_product_id;
$db->query( $q );
}

$fields =array( 'order_status'=> $d["order_status"],
'mdate'=> $timestamp );
$db->buildQuery('UPDATE', '#__{vm}_order_item', $fields, 'WHERE order_item_id='.intval( $d['order_item_id'] ));
return $db->query() !== false;
}
//echo "RUNNING PCI COMPLIANCE";
      //make sure we are PCI DSS Compliant, eradicate customer credit card codes that have been processed.
      $this->pcidss_compliance();

      return true;
}

function pcidss_compliance(){

/* For PCI compliance we may only store the last four digits and the CVV must not be stored, setting to general 000,
convert the credit card number to ############1234 (keep last four digits for future discussions with customer,
'the card ends in.....'
*/

$q = "SELECT `#__{vm}_order_payment`.`order_id`, order_payment_code,
".VM_DECRYPT_FUNCTION."(order_payment_number,'".ENCODE_KEY."') AS creditCardNumber,
CHAR_LENGTH(".VM_DECRYPT_FUNCTION."(order_payment_number,'".ENCODE_KEY."')) AS creditCardLength
FROM
`#__{vm}_order_payment` LEFT JOIN `#__{vm}_orders` ON
   `#__{vm}_order_payment`.`order_id` = `#__{vm}_orders`.`order_id`
WHERE
order_status != 'P'
AND CHAR_LENGTH(".VM_DECRYPT_FUNCTION."( order_payment_number, '".ENCODE_KEY."' )) > 8
AND LEFT( ".VM_DECRYPT_FUNCTION."( order_payment_number, '".ENCODE_KEY."' ), 8 ) != '########'";

$db = new ps_DB;
$dbUpdate = new ps_DB;

$mask = '##########################'; //24 mask characters, most cards are 16 but some are 17/18 so built in extra

$db->query( $q );

// Now update each ordered product
while( $db->next_record() ) {

//print "UPDATING ".$db->f("order_id");

$q = "UPDATE #__{vm}_order_payment SET
order_payment_code = '000', order_payment_number =
".
VM_ENCRYPT_FUNCTION."('".substr($mask, 0, $db->f("creditCardLength") - 4) . substr($db->f("creditCardNumber"), -4)."','".ENCODE_KEY."')
WHERE order_id = ".$db->f("order_id").";";

//echo $q."<br/>";

$dbUpdate->query( $q );
}
}

I don't know programming lingo, Is this code correct?
Joomla Version 1.5.23
VirtueMart 1.1.9 stable
Apache: 1.3.42
MySQL 5.0.91
PHP: 5.2.5
phpMyAdmin 3.3.10.2

chris.t.uk

  • Contributing Developer
  • Jr. Member
  • *
  • Posts: 98
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #4 on: February 24, 2010, 22:07:43 pm »
If the order status is anything but Pending (P) it will be rewritten, as it should no longer be needed (paper copies should now exist as you will have processed the order.

You can change the name from 'Pending' to 'WHATEVER', just so long as the 'P' status code remains as 'P' it will be fine.

What was the internal server error?
Hacks
Export Orders & Batch Update -> http://forum.virtuemart.net/index.php?topic=52215.0
UK Counties -> http://forum.virtuemart.net/index.php?topic=33968.0
Sites running Virtuemart
http://www.ladders247.co.uk http://www.tools247.co.uk http://www.mackay.co.uk
Synchronization
Synchronized with our instore system via PrePrepSQL and missing Image Remote Retrieval system ;-)

mercomarine

  • Jr. Member
  • **
  • Posts: 129
    • Merco Marine
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #5 on: February 28, 2010, 08:00:35 am »
No worries, I tried it a second time from scratch and it worked great! I must have been in a tired/ funky mood the other day. Thanks for this!
Joomla Version 1.5.23
VirtueMart 1.1.9 stable
Apache: 1.3.42
MySQL 5.0.91
PHP: 5.2.5
phpMyAdmin 3.3.10.2

AH

  • Global Moderator
  • Sr. Member
  • *
  • Posts: 2520
  • VirtueMart Version: 3.0.19.8
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #6 on: March 12, 2010, 19:05:55 pm »
Have a quick visit here to see what you need to become compliant.

https://www.pcisecuritystandards.org/saq/index.shtml

regards
A

Joomla 3.6.5
php 7

losmarinos3

  • Beginner
  • *
  • Posts: 19
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #7 on: March 05, 2011, 01:19:57 am »
Chris,
I am using the PayPal Pro payment method. Our Customers are getting a Conformation email showing their CVV" number and the expiry date of their Card.
AS I am sure you will know, this is totally against proper Protocol.
Is there  a way to to stop this information being sent out by insecure emails to our customers.
I apologises if this answer is already in the forum, but I did look hard for it, but no luck.

chris.t.uk

  • Contributing Developer
  • Jr. Member
  • *
  • Posts: 98
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #8 on: March 05, 2011, 08:22:32 am »
Hi losmarinos3

I don't use Paypal Pro, so forgive me if this sounds stupid, but is the email sent from Virtuemart or Paypal?

If it is sent from Virtuemart then would need to just find the right emailling function/template and remove the CVV.

Chris
Hacks
Export Orders & Batch Update -> http://forum.virtuemart.net/index.php?topic=52215.0
UK Counties -> http://forum.virtuemart.net/index.php?topic=33968.0
Sites running Virtuemart
http://www.ladders247.co.uk http://www.tools247.co.uk http://www.mackay.co.uk
Synchronization
Synchronized with our instore system via PrePrepSQL and missing Image Remote Retrieval system ;-)

losmarinos3

  • Beginner
  • *
  • Posts: 19
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #9 on: March 05, 2011, 15:26:50 pm »
Chris, Sorry I should have mentioned that is is the Customer email that goes out from VM and has the CVV number and expiry date on it.
PayPal would not do that I don't think.

AH

  • Global Moderator
  • Sr. Member
  • *
  • Posts: 2520
  • VirtueMart Version: 3.0.19.8
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #10 on: March 06, 2011, 11:33:30 am »
losmarinos you are correct that this is agains PCI compliance.

This is due to code in ps_checkout.php

Code: [Select]
        // Email Addresses for shopper and vendor
        // **************************************
        $shopper_email = $dbbt->f("user_email");
        $shopper_name = $dbbt->f("first_name")." ".$dbbt->f("last_name");

        $from_email = $dbv->f("contact_email");

        $shopper_subject = $dbv->f("vendor_name") . " ".$VM_LANG->_('PHPSHOP_ORDER_PRINT_PO_LBL',false)." - " . $db->f("order_id");
        $vendor_subject = $dbv->f("vendor_name") . " ".$VM_LANG->_('PHPSHOP_ORDER_PRINT_PO_LBL',false)." - " . $db->f("order_id");

        $shopper_order_link = $sess->url( SECUREURL ."index.php?page=account.order_details&order_id=$order_id", true, false );
        $vendor_order_link = $sess->url( SECUREURL ."index2.php?page=order.order_print&order_id=$order_id&pshop_mode=admin", true, false );

        /**
         * Prepare the payment information, including Credit Card information when not empty
         */
        $payment_info_details = $db_payment->f("payment_method_name");
        if( !empty( $_SESSION['ccdata']['order_payment_name'] )
            && !empty($_SESSION['ccdata']['order_payment_number'])) {
              $payment_info_details .= '<br />'.$VM_LANG->_('PHPSHOP_CHECKOUT_CONF_PAYINFO_NAMECARD',false).': '.$_SESSION['ccdata']['order_payment_name'].'<br />';
              $payment_info_details .= $VM_LANG->_('PHPSHOP_CHECKOUT_CONF_PAYINFO_CCNUM',false).': '.$this->asterisk_pad($_SESSION['ccdata']['order_payment_number'], 4 ).'<br />';
              $payment_info_details .= $VM_LANG->_('PHPSHOP_CHECKOUT_CONF_PAYINFO_EXDATE',false).': '.$_SESSION['ccdata']['order_payment_expire_month'].' / '.$_SESSION['ccdata']['order_payment_expire_year'].'<br />';
              if( !empty($_SESSION['ccdata']['credit_card_code'])) {
                  $payment_info_details .= 'CVV code: '.$_SESSION['ccdata']['credit_card_code'].'<br />';
              }

comment out the CVV lines.

Code: [Select]
              //if( !empty($_SESSION['ccdata']['credit_card_code'])) {
              //    $payment_info_details .= 'CVV code: '.$_SESSION['ccdata']['credit_card_code'].'<br />';


You could always create your own extended class function rather than messing with core code!



regards
A

Joomla 3.6.5
php 7

jenkinhill

  • UK Web Developer & Consultant
  • Global Moderator
  • Super Hero
  • *
  • Posts: 25538
  • Always on vacation
    • Jenkin Hill Internet
Kelvyn

Jenkin Hill Internet,
Keswick, Lake District

Unsolicited PMs/emails will be ignored.

Please mention your VirtueMart, Joomla and PHP versions when asking a question in this forum

Currently using VM3.2.0 on Joomla 3.6.5 PHP 7.0.12

Testing VM3.2.1 on J!3.6.5 and J!3.7B3

zanardi

  • Contributing Developer
  • Full Member
  • *
  • Posts: 878
    • GiBiLogic
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #12 on: April 02, 2011, 11:46:08 am »
Just FYI, those two lines have been removed from ps_checkout in release 1.1.8.
--
Francesco (zanardi)
http://extensions.gibilogic.com
@gibilogic on Twitter

jenkinhill

  • UK Web Developer & Consultant
  • Global Moderator
  • Super Hero
  • *
  • Posts: 25538
  • Always on vacation
    • Jenkin Hill Internet
Re: PCI DSS - Make Virtuemart Compliant Code
« Reply #13 on: January 02, 2012, 12:59:52 pm »

A reminder that as from 1 January 2012 all Payment Card Industry Data Security Standard (PCI DSS) assessments must be against version 2.0 of the standard. "PCI DSS requirements must be implemented by all entities that process, store or transmit account data"

Now may be a good time to check your on-line and off-line systems.

https://www.pcisecuritystandards.org/merchants/index.php

http://en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard
Kelvyn

Jenkin Hill Internet,
Keswick, Lake District

Unsolicited PMs/emails will be ignored.

Please mention your VirtueMart, Joomla and PHP versions when asking a question in this forum

Currently using VM3.2.0 on Joomla 3.6.5 PHP 7.0.12

Testing VM3.2.1 on J!3.6.5 and J!3.7B3