CVE-2022-29254 (published 2022-06-09T07:15Z)

silverstripe-omnipay is a SilverStripe integration with Omnipay PHP payments library. For a subset of Omnipay gateways (those that use intermediary states like `isNotification()` or `isRedirect()`), if the payment identifier or success URL is exposed it is possible for payments to be prematurely marked as completed without payment being taken. This is mitigated by the fact that most payment gateways hide this information from users, however some issuing banks offer flawed 3DSecure implementations that may inadvertently expose this data. The following versions have been patched to fix this issue: `2.5.2`, `3.0.2`, `3.1.4`, and `3.2.1`. There are no known workarounds for this vulnerability.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-436
cwe_name: Interpretation Conflict
description: Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.
extended_description: This is generally found in proxies, firewalls, anti-virus software, and other intermediary devices that monitor, allow, deny, or modify traffic based on how the client or server is expected to behave.
         

Fix information:
repo_url: https://github.com/silverstripe/silverstripe-omnipay
commit_details: repo_url: https://github.com/silverstripe/silverstripe-omnipay
author: Loz Calver
author_date: 2022-04-05 17:51:13+01:00
msg: [CVE-2022-29254] Add extra validation on payment completion
repository: repo_url: https://github.com/silverstripe/silverstripe-omnipay
repo_name: silverstripe/silverstripe-omnipay
description: Silverstripe integration with Omnipay PHP payments library.
repo_language: PHP
owner: silverstripe
file_changes: filename: AuthorizeService.php
change_type: ModificationType.MODIFY
diff: @@ -2,15 +2,15 @@
 
 namespace SilverStripe\Omnipay\Service;
 
-use SilverStripe\Omnipay\Exception\InvalidStateException;
 use SilverStripe\Omnipay\Exception\InvalidConfigurationException;
+use SilverStripe\Omnipay\Exception\InvalidStateException;
 use SilverStripe\Omnipay\Helper\ErrorHandling;
-use SilverStripe\Omnipay\Model\Message\AuthorizeRequest;
 use SilverStripe\Omnipay\Model\Message\AuthorizedResponse;
-use SilverStripe\Omnipay\Model\Message\CompleteAuthorizeError;
 use SilverStripe\Omnipay\Model\Message\AuthorizeError;
 use SilverStripe\Omnipay\Model\Message\AuthorizeRedirectResponse;
+use SilverStripe\Omnipay\Model\Message\AuthorizeRequest;
 use SilverStripe\Omnipay\Model\Message\AwaitingAuthorizeResponse;
+use SilverStripe\Omnipay\Model\Message\CompleteAuthorizeError;
 use SilverStripe\Omnipay\Model\Message\CompleteAuthorizeRequest;
 
 class AuthorizeService extends PaymentService
@@ -67,7 +67,7 @@ class AuthorizeService extends PaymentService
             );
         } elseif ($serviceResponse->isError()) {
             $this->createMessage(AuthorizeError::class, $response);
-        } else {
+        } elseif ($serviceResponse->isSuccessful()) {
             $this->markCompleted('Authorized', $serviceResponse, $response);
         }
 
@@ -118,15 +118,12 @@ class AuthorizeService extends PaymentService
 
         $serviceResponse = $this->wrapOmnipayResponse($response, $isNotification);
 
-        if ($serviceResponse->isError()) {
+        if ($serviceResponse->isAwaitingNotification()) {
+            ErrorHandling::safeExtend($this->payment, 'onAwaitingAuthorized', $serviceResponse);
+        } elseif ($serviceResponse->isError()) {
             $this->createMessage(CompleteAuthorizeError::class, $response);
-            return $serviceResponse;
-        }
-
-        if (!$serviceResponse->isAwaitingNotification()) {
+        } elseif ($serviceResponse->isSuccessful()) {
             $this->markCompleted('Authorized', $serviceResponse, $response);
-        } else {
-            ErrorHandling::safeExtend($this->payment, 'onAwaitingAuthorized', $serviceResponse);
         }
 
         return $serviceResponse;

programming_language: PHP
filename: CaptureService.php
change_type: ModificationType.MODIFY
diff: @@ -138,7 +138,9 @@ class CaptureService extends NotificationCompleteService
 
         $serviceResponse = $this->wrapOmnipayResponse($response);
 
-        if ($serviceResponse->isAwaitingNotification()) {
+        if ($serviceResponse->isError()) {
+            $this->createMessage($this->errorMessageType, $response);
+        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
             if ($diff < 0) {
                 $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
             } elseif ($diff > 0) {
@@ -146,17 +148,13 @@ class CaptureService extends NotificationCompleteService
             }
             $this->payment->Status = $this->pendingState;
             $this->payment->write();
-        } else {
-            if ($serviceResponse->isError()) {
-                $this->createMessage($this->errorMessageType, $response);
-            } else {
-                if ($diff < 0) {
-                    $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
-                } elseif ($diff > 0) {
-                    $this->createPartialPayment($diff, $this->pendingState);
-                }
-                $this->markCompleted($this->endState, $serviceResponse, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
+            if ($diff < 0) {
+                $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
+            } elseif ($diff > 0) {
+                $this->createPartialPayment($diff, $this->pendingState);
             }
+            $this->markCompleted($this->endState, $serviceResponse, $response);
         }
 
         return $serviceResponse;

programming_language: PHP
filename: CreateCardService.php
change_type: ModificationType.MODIFY
diff: @@ -3,14 +3,19 @@
 namespace SilverStripe\Omnipay\Service;
 
 use Omnipay\Common\Message\RequestInterface;
-use SilverStripe\Omnipay\Exception\InvalidStateException;
 use SilverStripe\Omnipay\Exception\InvalidConfigurationException;
+use SilverStripe\Omnipay\Exception\InvalidStateException;
 use SilverStripe\Omnipay\Helper\ErrorHandling;
-use SilverStripe\Omnipay\Model\Message;
+use SilverStripe\Omnipay\Model\Message\AwaitingCreateCardResponse;
+use SilverStripe\Omnipay\Model\Message\CompleteCreateCardError;
+use SilverStripe\Omnipay\Model\Message\CompleteCreateCardRequest;
+use SilverStripe\Omnipay\Model\Message\CreateCardError;
+use SilverStripe\Omnipay\Model\Message\CreateCardRedirectResponse;
+use SilverStripe\Omnipay\Model\Message\CreateCardRequest;
+use SilverStripe\Omnipay\Model\Message\CreateCardResponse;
 
 class CreateCardService extends PaymentService
 {
-
     /**
      * Start a createcard request
      *
@@ -39,12 +44,12 @@ class CreateCardService extends PaymentService
         $request = $this->oGateway()->createCard($gatewayData);
         $this->extend('onAfterCreateCard', $request);
 
-        $this->createMessage(Message\CreateCardRequest::class, $request);
+        $this->createMessage(CreateCardRequest::class, $request);
 
         try {
             $response = $this->response = $request->send();
         } catch (\Omnipay\Common\Exception\OmnipayException $e) {
-            $this->createMessage(Message\CreateCardError::class, $e);
+            $this->createMessage(CreateCardError::class, $e);
             // create an error response
             return $this->generateServiceResponse(ServiceResponse::SERVICE_ERROR);
         }
@@ -58,12 +63,12 @@ class CreateCardService extends PaymentService
             $this->payment->write();
 
             $this->createMessage(
-                $serviceResponse->isRedirect() ? Message\CreateCardRedirectResponse::class : Message\AwaitingCreateCardResponse::class,
+                $serviceResponse->isRedirect() ? CreateCardRedirectResponse::class : AwaitingCreateCardResponse::class,
                 $response
             );
         } elseif ($serviceResponse->isError()) {
-            $this->createMessage(Message\CreateCardError::class, $response);
-        } else {
+            $this->createMessage(CreateCardError::class, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
             $this->markCompleted('CardCreated', $serviceResponse, $response);
         }
 
@@ -103,26 +108,23 @@ class CreateCardService extends PaymentService
         $request = $gateway->completeCreateCard($gatewayData);
         $this->extend('onAfterCompleteCreateCard', $request);
 
-        $this->createMessage(Message\CompleteCreateCardRequest::class, $request);
+        $this->createMessage(CompleteCreateCardRequest::class, $request);
         $response = null;
         try {
             $response       = $this->response = $request->send();
         } catch (\Omnipay\Common\Exception\OmnipayException $e) {
-            $this->createMessage(Message\CompleteCreateCardError::class, $e);
+            $this->createMessage(CompleteCreateCardError::class, $e);
             return $this->generateServiceResponse($flags | ServiceResponse::SERVICE_ERROR);
         }
 
         $serviceResponse = $this->wrapOmnipayResponse($response, $isNotification);
 
-        if ($serviceResponse->isError()) {
-            $this->createMessage(Message\CompleteCreateCardError::class, $response);
-            return $serviceResponse;
-        }
-
-        if (!$serviceResponse->isAwaitingNotification()) {
-            $this->markCompleted('CardCreated', $serviceResponse, $response);
-        } else {
+        if ($serviceResponse->isAwaitingNotification()) {
             ErrorHandling::safeExtend($this->payment, 'onAwaitingCreateCard', $serviceResponse);
+        } elseif ($serviceResponse->isError()) {
+            $this->createMessage(CompleteCreateCardError::class, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
+            $this->markCompleted('CardCreated', $serviceResponse, $response);
         }
 
         return $serviceResponse;
@@ -131,7 +133,7 @@ class CreateCardService extends PaymentService
     protected function markCompleted($endStatus, ServiceResponse $serviceResponse, $gatewayMessage)
     {
         parent::markCompleted($endStatus, $serviceResponse, $gatewayMessage);
-        $this->createMessage(Message\CreateCardResponse::class, $gatewayMessage);
+        $this->createMessage(CreateCardResponse::class, $gatewayMessage);
         ErrorHandling::safeExtend($this->payment, 'onCardCreated', $serviceResponse);
     }
 }

programming_language: PHP
filename: NotificationCompleteService.php
change_type: ModificationType.MODIFY
diff: @@ -79,7 +79,10 @@ abstract class NotificationCompleteService extends PaymentService
         }
 
         // check if we're done
-        if (!$serviceResponse->isError() && !$serviceResponse->isAwaitingNotification()) {
+        if (!$serviceResponse->isError()
+            && !$serviceResponse->isAwaitingNotification()
+            && $serviceResponse->isSuccessful()
+        ) {
             $this->markCompleted($this->endState, $serviceResponse, $serviceResponse->getOmnipayResponse());
         }
 

programming_language: PHP
filename: PurchaseService.php
change_type: ModificationType.MODIFY
diff: @@ -5,7 +5,13 @@ namespace SilverStripe\Omnipay\Service;
 use SilverStripe\Omnipay\Exception\InvalidStateException;
 use SilverStripe\Omnipay\Exception\InvalidConfigurationException;
 use SilverStripe\Omnipay\Helper\ErrorHandling;
-use SilverStripe\Omnipay\Model\Message;
+use SilverStripe\Omnipay\Model\Message\AwaitingPurchaseResponse;
+use SilverStripe\Omnipay\Model\Message\CompletePurchaseError;
+use SilverStripe\Omnipay\Model\Message\CompletePurchaseRequest;
+use SilverStripe\Omnipay\Model\Message\PurchasedResponse;
+use SilverStripe\Omnipay\Model\Message\PurchaseError;
+use SilverStripe\Omnipay\Model\Message\PurchaseRedirectResponse;
+use SilverStripe\Omnipay\Model\Message\PurchaseRequest;
 
 class PurchaseService extends PaymentService
 {
@@ -43,12 +49,12 @@ class PurchaseService extends PaymentService
         $request = $this->oGateway()->purchase($gatewayData);
         $this->extend('onAfterPurchase', $request);
 
-        $this->createMessage(Message\PurchaseRequest::class, $request);
+        $this->createMessage(PurchaseRequest::class, $request);
 
         try {
             $response = $this->response = $request->send();
         } catch (\Omnipay\Common\Exception\OmnipayException $e) {
-            $this->createMessage(Message\PurchaseError::class, $e);
+            $this->createMessage(PurchaseError::class, $e);
             // create an error response
             return $this->generateServiceResponse(ServiceResponse::SERVICE_ERROR);
         }
@@ -62,12 +68,12 @@ class PurchaseService extends PaymentService
             $this->payment->write();
 
             $this->createMessage(
-                $serviceResponse->isRedirect() ? Message\PurchaseRedirectResponse::class : Message\AwaitingPurchaseResponse::class,
+                $serviceResponse->isRedirect() ? PurchaseRedirectResponse::class : AwaitingPurchaseResponse::class,
                 $response
             );
         } elseif ($serviceResponse->isError()) {
-            $this->createMessage(Message\PurchaseError::class, $response);
-        } else {
+            $this->createMessage(PurchaseError::class, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
             $this->markCompleted('Captured', $serviceResponse, $response);
         }
 
@@ -105,36 +111,32 @@ class PurchaseService extends PaymentService
         $request = $gateway->completePurchase($gatewayData);
         $this->extend('onAfterCompletePurchase', $request);
 
-        $this->createMessage(Message\CompletePurchaseRequest::class, $request);
+        $this->createMessage(CompletePurchaseRequest::class, $request);
         $response = null;
         try {
             $response = $this->response = $request->send();
         } catch (\Omnipay\Common\Exception\OmnipayException $e) {
-            $this->createMessage(Message\CompletePurchaseError::class, $e);
+            $this->createMessage(CompletePurchaseError::class, $e);
             return $this->generateServiceResponse($flags | ServiceResponse::SERVICE_ERROR);
         }
 
         $serviceResponse = $this->wrapOmnipayResponse($response, $isNotification);
-        if ($serviceResponse->isError()) {
-            $this->createMessage(Message\CompletePurchaseError::class, $response);
-            return $serviceResponse;
-        }
 
-        // only update payment status if we're not waiting for a notification
-        if (!$serviceResponse->isAwaitingNotification()) {
-            $this->markCompleted('Captured', $serviceResponse, $response);
-        } else {
+        if ($serviceResponse->isAwaitingNotification()) {
             ErrorHandling::safeExtend($this->payment, 'onAwaitingCaptured', $serviceResponse);
+        } elseif ($serviceResponse->isError()) {
+            $this->createMessage(CompletePurchaseError::class, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
+            $this->markCompleted('Captured', $serviceResponse, $response);
         }
 
-
         return $serviceResponse;
     }
 
     protected function markCompleted($endStatus, ServiceResponse $serviceResponse, $gatewayMessage)
     {
         parent::markCompleted($endStatus, $serviceResponse, $gatewayMessage);
-        $this->createMessage(Message\PurchasedResponse::class, $gatewayMessage);
+        $this->createMessage(PurchasedResponse::class, $gatewayMessage);
         ErrorHandling::safeExtend($this->payment, 'onCaptured', $serviceResponse);
     }
 }

programming_language: PHP
filename: RefundService.php
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,10 @@ use SilverStripe\Omnipay\Exception\MissingParameterException;
 use SilverStripe\Omnipay\GatewayInfo;
 use SilverStripe\Omnipay\Helper\ErrorHandling;
 use SilverStripe\Omnipay\Helper\PaymentMath;
-use SilverStripe\Omnipay\Model\Message;
+use SilverStripe\Omnipay\Model\Message\PartiallyRefundedResponse;
+use SilverStripe\Omnipay\Model\Message\RefundedResponse;
+use SilverStripe\Omnipay\Model\Message\RefundError;
+use SilverStripe\Omnipay\Model\Message\RefundRequest;
 use SilverStripe\Omnipay\Model\Payment;
 
 class RefundService extends NotificationCompleteService
@@ -20,9 +23,9 @@ class RefundService extends NotificationCompleteService
 
     protected $pendingState = 'PendingRefund';
 
-    protected $requestMessageType = Message\RefundRequest::class;
+    protected $requestMessageType = RefundRequest::class;
 
-    protected $errorMessageType = Message\RefundError::class;
+    protected $errorMessageType = RefundError::class;
 
     /**
      * Return money to the previously charged credit card.
@@ -127,21 +130,20 @@ class RefundService extends NotificationCompleteService
 
         $serviceResponse = $this->wrapOmnipayResponse($response);
 
-        if ($serviceResponse->isAwaitingNotification()) {
+        if ($serviceResponse->isError()) {
+            $this->createMessage($this->errorMessageType, $response);
+        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
             if ($isPartial) {
                 $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
             }
             $this->payment->Status = $this->pendingState;
             $this->payment->write();
-        } else {
-            if ($serviceResponse->isError()) {
-                $this->createMessage($this->errorMessageType, $response);
-            } else {
-                if ($isPartial) {
-                    $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
-                }
-                $this->markCompleted($this->endState, $serviceResponse, $response);
+        } elseif ($serviceResponse->isSuccessful()) {
+            if ($isPartial) {
+                $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);
             }
+
+            $this->markCompleted($this->endState, $serviceResponse, $response);
         }
 
         return $serviceResponse;
@@ -182,9 +184,9 @@ class RefundService extends NotificationCompleteService
 
         parent::markCompleted($endStatus, $serviceResponse, $gatewayMessage);
         if ($endStatus === 'Captured') {
-            $this->createMessage(Message\PartiallyRefundedResponse::class, $gatewayMessage);
+            $this->createMessage(PartiallyRefundedResponse::class, $gatewayMessage);
         } else {
-            $this->createMessage(Message\RefundedResponse::class, $gatewayMessage);
+            $this->createMessage(RefundedResponse::class, $gatewayMessage);
         }
 
         ErrorHandling::safeExtend($this->payment, 'onRefunded', $serviceResponse);

programming_language: PHP
filename: ServiceResponse.php
change_type: ModificationType.MODIFY
diff: @@ -97,6 +97,22 @@ class ServiceResponse
         return $this->payment;
     }
 
+    /**
+     * Whether the response is marked as successful by Omnipay.
+     *
+     * @return bool
+     */
+    public function isSuccessful()
+    {
+        if ($this->omnipayResponse instanceof NotificationInterface) {
+            return $this->omnipayResponse->getTransactionStatus() === NotificationInterface::STATUS_COMPLETED;
+        } elseif ($this->omnipayResponse instanceof AbstractResponse) {
+            return $this->omnipayResponse->isSuccessful();
+        }
+
+        return false;
+    }
+
     /**
      * Whether or not this is an *offsite* redirect.
      * This is only the case when there's an Omnipay response present that *is* a redirect.

programming_language: PHP
filename: VoidService.php
change_type: ModificationType.MODIFY
diff: @@ -2,12 +2,14 @@
 
 namespace SilverStripe\Omnipay\Service;
 
+use Omnipay\Common\Exception\OmnipayException;
 use SilverStripe\Omnipay\Exception\InvalidConfigurationException;
 use SilverStripe\Omnipay\Exception\MissingParameterException;
-use Omnipay\Common\Exception\OmnipayException;
 use SilverStripe\Omnipay\GatewayInfo;
 use SilverStripe\Omnipay\Helper\ErrorHandling;
-use SilverStripe\Omnipay\Model\Message;
+use SilverStripe\Omnipay\Model\Message\VoidedResponse;
+use SilverStripe\Omnipay\Model\Message\VoidError;
+use SilverStripe\Omnipay\Model\Message\VoidRequest;
 
 class VoidService extends NotificationCompleteService
 {
@@ -17,9 +19,9 @@ class VoidService extends NotificationCompleteService
 
     protected $pendingState = 'PendingVoid';
 
-    protected $requestMessageType = Message\VoidRequest::class;
+    protected $requestMessageType = VoidRequest::class;
 
-    protected $errorMessageType = Message\VoidError::class;
+    protected $errorMessageType = VoidError::class;
 
     /**
      * Void/cancel a payment
@@ -97,15 +99,13 @@ class VoidService extends NotificationCompleteService
 
         $serviceResponse = $this->wrapOmnipayResponse($response);
 
-        if ($serviceResponse->isAwaitingNotification()) {
+        if ($serviceResponse->isError()) {
+            $this->createMessage($this->errorMessageType, $response);
+        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
             $this->payment->Status = $this->pendingState;
             $this->payment->write();
-        } else {
-            if ($serviceResponse->isError()) {
-                $this->createMessage($this->errorMessageType, $response);
-            } else {
-                $this->markCompleted($this->endState, $serviceResponse, $response);
-            }
+        } elseif ($serviceResponse->isSuccessful()) {
+            $this->markCompleted($this->endState, $serviceResponse, $response);
         }
 
         return $serviceResponse;
@@ -114,7 +114,7 @@ class VoidService extends NotificationCompleteService
     protected function markCompleted($endStatus, ServiceResponse $serviceResponse, $gatewayMessage)
     {
         parent::markCompleted($endStatus, $serviceResponse, $gatewayMessage);
-        $this->createMessage(Message\VoidedResponse::class, $gatewayMessage);
+        $this->createMessage(VoidedResponse::class, $gatewayMessage);
 
         ErrorHandling::safeExtend($this->payment, 'onVoid', $serviceResponse);
     }

programming_language: PHP