CVE-2022-29254 (PHP, published 2022-06-09T07:15Z)
Description: value: 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 v3 base score: 6.5)
Weakness: CWE-436 \u2014 Interpretation Conflict
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.
Repository: https://github.com/silverstripe/silverstripe-omnipay
Files changed:
src/Service/AuthorizeService.php
src/Service/CaptureService.php
src/Service/CreateCardService.php
src/Service/NotificationCompleteService.php
src/Service/PurchaseService.php
src/Service/RefundService.php
src/Service/ServiceResponse.php
src/Service/VoidService.php
Commit message: [CVE-2022-29254] Add extra validation on payment completion


Vulnerable code:
use SilverStripe\Omnipay\Exception\InvalidStateException;
use SilverStripe\Omnipay\Model\Message\AuthorizeRequest;
use SilverStripe\Omnipay\Model\Message\CompleteAuthorizeError;
        } else {
        if ($serviceResponse->isError()) {
            return $serviceResponse;
        }

        if (!$serviceResponse->isAwaitingNotification()) {
        } else {
            ErrorHandling::safeExtend($this->payment, 'onAwaitingAuthorized', $serviceResponse);
        if ($serviceResponse->isAwaitingNotification()) {
        } 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);
use SilverStripe\Omnipay\Exception\InvalidStateException;
use SilverStripe\Omnipay\Model\Message;

        $this->createMessage(Message\CreateCardRequest::class, $request);
            $this->createMessage(Message\CreateCardError::class, $e);
                $serviceResponse->isRedirect() ? Message\CreateCardRedirectResponse::class : Message\AwaitingCreateCardResponse::class,
            $this->createMessage(Message\CreateCardError::class, $response);
        } else {
        $this->createMessage(Message\CompleteCreateCardRequest::class, $request);
            $this->createMessage(Message\CompleteCreateCardError::class, $e);
        if ($serviceResponse->isError()) {
            $this->createMessage(Message\CompleteCreateCardError::class, $response);
            return $serviceResponse;
        }

        if (!$serviceResponse->isAwaitingNotification()) {
            $this->markCompleted('CardCreated', $serviceResponse, $response);
        } else {
        $this->createMessage(Message\CreateCardResponse::class, $gatewayMessage);
        if (!$serviceResponse->isError() && !$serviceResponse->isAwaitingNotification()) {
use SilverStripe\Omnipay\Model\Message;
        $this->createMessage(Message\PurchaseRequest::class, $request);
            $this->createMessage(Message\PurchaseError::class, $e);
                $serviceResponse->isRedirect() ? Message\PurchaseRedirectResponse::class : Message\AwaitingPurchaseResponse::class,
            $this->createMessage(Message\PurchaseError::class, $response);
        } else {
        $this->createMessage(Message\CompletePurchaseRequest::class, $request);
            $this->createMessage(Message\CompletePurchaseError::class, $e);
        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 {

        $this->createMessage(Message\PurchasedResponse::class, $gatewayMessage);
use SilverStripe\Omnipay\Model\Message;
    protected $requestMessageType = Message\RefundRequest::class;
    protected $errorMessageType = Message\RefundError::class;
        if ($serviceResponse->isAwaitingNotification()) {
        } 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);
            $this->createMessage(Message\PartiallyRefundedResponse::class, $gatewayMessage);
            $this->createMessage(Message\RefundedResponse::class, $gatewayMessage);
use Omnipay\Common\Exception\OmnipayException;
use SilverStripe\Omnipay\Model\Message;
    protected $requestMessageType = Message\VoidRequest::class;
    protected $errorMessageType = Message\VoidError::class;
        if ($serviceResponse->isAwaitingNotification()) {
        } else {
            if ($serviceResponse->isError()) {
                $this->createMessage($this->errorMessageType, $response);
            } else {
                $this->markCompleted($this->endState, $serviceResponse, $response);
            }
        $this->createMessage(Message\VoidedResponse::class, $gatewayMessage);

Fixed code:
use SilverStripe\Omnipay\Exception\InvalidStateException;
use SilverStripe\Omnipay\Model\Message\AuthorizeRequest;
use SilverStripe\Omnipay\Model\Message\CompleteAuthorizeError;
        } elseif ($serviceResponse->isSuccessful()) {
        if ($serviceResponse->isAwaitingNotification()) {
            ErrorHandling::safeExtend($this->payment, 'onAwaitingAuthorized', $serviceResponse);
        } elseif ($serviceResponse->isError()) {
        } elseif ($serviceResponse->isSuccessful()) {
        if ($serviceResponse->isError()) {
            $this->createMessage($this->errorMessageType, $response);
        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
        } 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);
use SilverStripe\Omnipay\Exception\InvalidStateException;
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;
        $this->createMessage(CreateCardRequest::class, $request);
            $this->createMessage(CreateCardError::class, $e);
                $serviceResponse->isRedirect() ? CreateCardRedirectResponse::class : AwaitingCreateCardResponse::class,
            $this->createMessage(CreateCardError::class, $response);
        } elseif ($serviceResponse->isSuccessful()) {
        $this->createMessage(CompleteCreateCardRequest::class, $request);
            $this->createMessage(CompleteCreateCardError::class, $e);
        if ($serviceResponse->isAwaitingNotification()) {
        } elseif ($serviceResponse->isError()) {
            $this->createMessage(CompleteCreateCardError::class, $response);
        } elseif ($serviceResponse->isSuccessful()) {
            $this->markCompleted('CardCreated', $serviceResponse, $response);
        $this->createMessage(CreateCardResponse::class, $gatewayMessage);
        if (!$serviceResponse->isError()
            && !$serviceResponse->isAwaitingNotification()
            && $serviceResponse->isSuccessful()
        ) {
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;
        $this->createMessage(PurchaseRequest::class, $request);
            $this->createMessage(PurchaseError::class, $e);
                $serviceResponse->isRedirect() ? PurchaseRedirectResponse::class : AwaitingPurchaseResponse::class,
            $this->createMessage(PurchaseError::class, $response);
        } elseif ($serviceResponse->isSuccessful()) {
        $this->createMessage(CompletePurchaseRequest::class, $request);
            $this->createMessage(CompletePurchaseError::class, $e);
        if ($serviceResponse->isAwaitingNotification()) {
        } elseif ($serviceResponse->isError()) {
            $this->createMessage(CompletePurchaseError::class, $response);
        } elseif ($serviceResponse->isSuccessful()) {
            $this->markCompleted('Captured', $serviceResponse, $response);
        $this->createMessage(PurchasedResponse::class, $gatewayMessage);
use SilverStripe\Omnipay\Model\Message\PartiallyRefundedResponse;
use SilverStripe\Omnipay\Model\Message\RefundedResponse;
use SilverStripe\Omnipay\Model\Message\RefundError;
use SilverStripe\Omnipay\Model\Message\RefundRequest;
    protected $requestMessageType = RefundRequest::class;
    protected $errorMessageType = RefundError::class;
        if ($serviceResponse->isError()) {
            $this->createMessage($this->errorMessageType, $response);
        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
        } elseif ($serviceResponse->isSuccessful()) {
            if ($isPartial) {
                $this->createPartialPayment(PaymentMath::multiply($amount, '-1'), $this->pendingState);

            $this->markCompleted($this->endState, $serviceResponse, $response);
            $this->createMessage(PartiallyRefundedResponse::class, $gatewayMessage);
            $this->createMessage(RefundedResponse::class, $gatewayMessage);
    /**
     * 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;
    }

use Omnipay\Common\Exception\OmnipayException;
use SilverStripe\Omnipay\Model\Message\VoidedResponse;
use SilverStripe\Omnipay\Model\Message\VoidError;
use SilverStripe\Omnipay\Model\Message\VoidRequest;
    protected $requestMessageType = VoidRequest::class;
    protected $errorMessageType = VoidError::class;
        if ($serviceResponse->isError()) {
            $this->createMessage($this->errorMessageType, $response);
        } elseif ($serviceResponse->isRedirect() || $serviceResponse->isAwaitingNotification()) {
        } elseif ($serviceResponse->isSuccessful()) {
            $this->markCompleted($this->endState, $serviceResponse, $response);
        $this->createMessage(VoidedResponse::class, $gatewayMessage);