/** * perform anti fraud checks on ipn values * * @param cbpaidPaymentNotification $ipn * @param cbpaidPaymentBasket $paymentBasket * @return bool|string */ private function _validateIPN( $ipn, $paymentBasket ) { global $_CB_database; $matching = true; if ( in_array( $ipn->payment_status, array( 'Completed', 'Processed', 'Canceled_Reversal' ) ) ) { if ( $ipn->txn_type == 'subscr_payment' ) { $payments = $paymentBasket->getPaymentsTotals( $ipn->txn_id ); if ( ( $paymentBasket->mc_amount1 != 0 ) && ( $payments->count == 0 ) ) { $amount = $paymentBasket->mc_amount1; } else { $amount = $paymentBasket->mc_amount3; } if ( sprintf( '%.2f', $ipn->mc_gross ) != sprintf( '%.2f', $amount ) ) { if ( ( sprintf( '%.2f', $ipn->mc_gross ) < sprintf( '%.2f', $amount ) ) || ( sprintf( '%.2f', ( $ipn->mc_gross - $ipn->tax ) ) != sprintf( '%.2f', $amount ) ) ) { if ( ( ! ( ( $paymentBasket->mc_amount1 != 0 ) && ( $payments->count == 0 ) ) ) && ( ( (float) sprintf( '%.2f', ( $ipn->mc_gross - abs( $ipn->tax ) ) ) ) < ( (float) sprintf( '%.2f', $amount ) ) ) ) { $matching = CBPTXT::P( 'amount mismatch on recurring_payment: $amount: [amount] != IPN mc_gross: [gross] or IPN mc_gross - IPN tax: [net] where IPN tax = [tax]', array( '[amount]' => $amount, '[net]' => ( $ipn->mc_gross - $ipn->tax ), '[gross]' => $ipn->mc_gross, '[tax]' => $ipn->tax ) ); } } } } else { if ( sprintf( '%.2f', $ipn->mc_gross ) != sprintf( '%.2f', $paymentBasket->mc_gross ) ) { if ( ( sprintf( '%.2f', $ipn->mc_gross ) < sprintf( '%.2f', $paymentBasket->mc_gross ) ) || ( sprintf( '%.2f', $ipn->mc_gross - $ipn->tax ) != sprintf( '%.2f', $paymentBasket->mc_gross ) ) ) { $matching = CBPTXT::P( 'amount mismatch on webaccept: BASKET mc_gross: [basket_gross] != IPN mc_gross: [gross] or IPN mc_gross - IPN tax: [net] where IPN tax = [tax]', array( '[basket_gross]' => $paymentBasket->mc_gross, '[net]' => ( $ipn->mc_gross - $ipn->tax ), '[gross]' => $ipn->mc_gross, '[tax]' => $ipn->tax ) ); } } } } if ( in_array( $ipn->txn_type, array( 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed', 'subscr_payment' ) ) ) { if ( ! $paymentBasket->isAnyAutoRecurring() ) { $matching = CBPTXT::P( 'paypal subscription IPN type [txn_type] for a basket without auto-recurring items', array( '[txn_type]' => $ipn->txn_type ) ); } } if ( ! in_array( $ipn->txn_type, array( 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed' ) ) ) { if ( ( $ipn->txn_id === '' ) || ( $ipn->txn_id === 0 ) || ( $ipn->txn_id === null ) ) { $matching = CBPTXT::T( 'illegal transaction id' ); } else { $countBaskets = $paymentBasket->countRows( "txn_id = '" . $_CB_database->getEscaped( $ipn->txn_id ) . "' AND payment_status = 'Completed'" ); if ( ( $countBaskets == 1 ) && ( $paymentBasket->txn_id != $ipn->txn_id ) || ( $countBaskets > 1 ) ) { $matching = CBPTXT::P( 'transaction already used for [count] other already completed payment(s)', array( '[count]' => $countBaskets ) ); } } } return $matching; }
/** * Checks against frauds for PDT and for IPN * * In order to prevent fraud, PayPal recommends that your programs verify the following: * When you receive a VERIFIED response, perform the following checks: * 1. Check that the payment_status is Completed. (NOT done here, but in UpdatePayment status) * 2. If the payment_status is Completed, check the txn_id against the previous completed PayPal * transaction you have processed to ensure it is not a duplicate. * 3. After you have checked the payment_status and txn_id, make sure the * receiver_email is an email address registered in your PayPal account. * 4. Check that the price, mc_gross, and currency, mc_currency, are correct for the item, * item_name or item_number. * 5. Check the the shared secret returned to you is correct. * * @param cbpaidPaymentNotification $ipn notification verified with paypal * @param cbpaidPaymentBasket $paymentBasket matched basket * @param string $cbpid shared secret which should be returned by paypal * @return boolean|string TRUE for no fraud detected, otherwise error TEXT */ private function _checkNotPayPalFraud( $ipn, $paymentBasket, $cbpid ) { global $_CB_database; $matching = true; // 3) receiver_email is an email address registered in your PayPal account, to prevent the payment // from being sent to a fraudulent account: $receiver_email = strtolower( trim( $this->getAccountParam( 'paypal_receiver_email' ) ) ); $business = strtolower( trim( $this->getAccountParam( 'paypal_business' ) ) ); if ( $receiver_email ) { if ( strtolower( $ipn->receiver_email ) != $receiver_email ) { // let's give a second, third and fourth chance to misconfigurations: if ( ( strtolower( $ipn->business ) != $business ) && ( strtolower( $ipn->receiver_email ) != $business ) && ( strtolower( $ipn->business ) != $receiver_email ) ) { $matching = sprintf( "receiver_email mismatch: parametered business (%s) or receiver (%s) expected does not match IPN business (%s) or receiver (%s).", $business, $receiver_email, $ipn->business, $ipn->receiver_email ); } } } else { if ( strtolower( $ipn->business ) != $business ) { // let's give a second chance to misconfigurations: if ( strtolower( $ipn->receiver_email ) != $business ) { $matching = sprintf( "business email mismatch: parametered business (%s) or receiver (%s) expected does not match IPN receiver (%s).", $business, $receiver_email, $ipn->receiver_email ); } } } // 4) Check transaction details, such as the item number and price, to confirm that the price has not // been changed: mc_gross, and currency, mc_currency, item_name or item_number: if ( in_array( $ipn->payment_status, array( 'Completed', 'Processed', 'Canceled_Reversal' ) ) ) { if ( $ipn->txn_type == 'subscr_payment' ) { $checkFields = array( 'mc_currency' => 100, 'item_name' => 127, 'item_number' => 127 ); $txt_error = "currency, item name or number mismatch"; // treat 'mc_gross': $payments = $paymentBasket->getPaymentsTotals( $ipn->txn_id ); if ( ( $paymentBasket->mc_amount1 != 0 ) && ( $payments->count == 0 ) ) { $tobepaid = $paymentBasket->mc_amount1; } else { $tobepaid = $paymentBasket->mc_amount3; } if ( sprintf( '%.2f', $ipn->mc_gross ) != sprintf( '%.2f', $tobepaid ) ) { // try to check if it's a paypal tax added in paypal: if ( ( sprintf( '%.2f', $ipn->mc_gross ) < sprintf( '%.2f', $tobepaid ) ) || ( sprintf( '%.2f', $ipn->mc_gross - $ipn->tax ) != sprintf( '%.2f', $tobepaid ) ) ) { // Final attempt to say "ok": if there is an increase in this recurring payment (not first one) done at paypal's side (20% per period max): if ( ( ! ( ( $paymentBasket->mc_amount1 != 0 ) && ( $payments->count == 0 ) ) ) && ( ( (float ) sprintf( '%.2f', $ipn->mc_gross - abs( $ipn->tax ) ) ) < (float) sprintf( '%.2f', $tobepaid ) ) ) { $matching = sprintf("amount mismatch on subscr_payment: tobepaid: %s != IPN mc_gross: %s or IPN mc_gross - IPN tax: %s where IPN tax = %s", $tobepaid, $ipn->mc_gross - $ipn->tax, $ipn->mc_gross, $ipn->tax ); } } } } else { // elseif ( $ipn->txn_type == 'web_accept' ) { if ( sprintf( '%.2f', $ipn->mc_gross ) != sprintf( '%.2f', $paymentBasket->mc_gross ) ) { // try to check if it's a paypal tax added in paypal: if ( ( sprintf( '%.2f', $ipn->mc_gross ) < sprintf( '%.2f', $paymentBasket->mc_gross ) ) || ( sprintf( '%.2f', $ipn->mc_gross - $ipn->tax ) != sprintf( '%.2f', $paymentBasket->mc_gross ) ) ) { $matching = sprintf("amount mismatch on webaccept: BASKET mc_gross: %s != IPN mc_gross: %s or IPN mc_gross - IPN tax: %s where IPN tax = %s", $paymentBasket->mc_gross, $ipn->mc_gross - $ipn->tax, $ipn->mc_gross, $ipn->tax ); } } $checkFields = array( 'mc_currency' => 100, 'item_name' => 127, 'item_number' => 127, 'quantity' => 127 ); if ( $ipn->payment_status == 'Canceled_Reversal' ) { // for some reasons, Cancel_Reversal (we won!) don't provide quantity, so do not check: (bug #1099) unset( $checkFields['quantity'] ); } $txt_error = "currency, item name or number or quantity mismatch"; } foreach ( $checkFields as $cf => $csize ) { if ( !isset( $ipn->$cf) || !isset( $paymentBasket->$cf) || ( trim( substr( $ipn->$cf, 0, $csize ) ) != trim( substr( $paymentBasket->$cf, 0, $csize ) ) ) ) { // print_r($ipn); print_r($paymentBasket); $matching = $txt_error . ': ' . sprintf( "IPN %s (%s) does not match basket %s (%s) nor their trimmed sizes for IPN (%s) and basket (%s)", $cf, $ipn->$cf, $cf, $paymentBasket->$cf, trim( substr( $ipn->$cf, 0, $csize ) ), trim( substr( $paymentBasket->$cf, 0, $csize ) ) ); break; } } } else { //TBD: see what to check for other events... } if ( in_array( $ipn->txn_type, array( 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed', 'subscr_payment' ) ) ) { if ( ! $paymentBasket->isAnyAutoRecurring() ) { $matching = sprintf( "paypal subscription IPN type %s for a basket without auto-recurring items", $ipn->txn_type ); } } // 2) txn_id is not a duplicate to prevent someone from reusing an old, completed transaction: if ( ! in_array( $ipn->txn_type, array( 'subscr_signup', 'subscr_modify', 'subscr_eot', 'subscr_cancel', 'subscr_failed' ) ) ) { if ( ( $ipn->txn_id === '' ) || ( $ipn->txn_id === 0 ) || ( $ipn->txn_id === null ) ) { $matching = "illegal transaction id"; } else { $countBaskets = $paymentBasket->countRows( "txn_id = '" . $_CB_database->getEscaped( $ipn->txn_id ) . "' AND payment_status = 'Completed'" ); if ( ( $countBaskets == 1 ) && ( $paymentBasket->txn_id != $ipn->txn_id ) || ( $countBaskets > 1 ) ) { $matching = sprintf( "transaction already used for %d other already completed payment(s)", $countBaskets ); } } } // 5) Check the the shared secret returned to you is correct. if ( $cbpid != $paymentBasket->shared_secret ) { $matching = sprintf( "shared secret '%s' returned by Paypal does not match the value we expected", htmlspecialchars( $cbpid ) ); } return $matching; }