The Syliud PayPal Plugin is the Sylius Core Team’s plugin for the PayPal Commerce Platform. Prior to 1.6.2, 1.7.2, and 2.0.2, a discovered…
GitHub_M·CWE-472·Published 2025-03-19
The Syliud PayPal Plugin is the Sylius Core Team’s plugin for the PayPal Commerce Platform. Prior to 1.6.2, 1.7.2, and 2.0.2, a discovered vulnerability allows users to modify their shopping cart after completing the PayPal Checkout process and payment authorization. If a user initiates a PayPal transaction from a product page or the cart page and then returns to the order summary page, they can still manipulate the cart contents before finalizing the order. As a result, the order amount in Sylius may be higher than the amount actually captured by PayPal, leading to a scenario where merchants deliver products or services without full payment. The issue is fixed in versions: 1.6.2, 1.7.2, 2.0.2 and above.
The Syliud PayPal Plugin is the Sylius Core Team’s plugin for the PayPal Commerce Platform. Prior to 1.6.2, 1.7.2, and 2.0.2, a discovered vulnerability allows users to modify their shopping cart after completing the PayPal Checkout process and payment authorization. If a user initiates a PayPal transaction from a product page or the cart page and then returns to the order summary page, they can still manipulate the cart contents before finalizing the order. As a result, the order amount in Sylius may be higher than the amount actually captured by PayPal, leading to a scenario where merchants deliver products or services without full payment. The issue is fixed in versions: 1.6.2, 1.7.2, 2.0.2 and above.
A discovered vulnerability allows users to modify their shopping cart after completing the PayPal Checkout process and payment authorization. If a user initiates a PayPal transaction from a product page or the cart page and then returns to the order summary page, they can still manipulate the cart contents before finalizing the order. As a result, the order amount in Sylius may be higher than the amount actually captured by PayPal, leading to a scenario where merchants deliver products or services without full payment. ### Impact - Users can exploit this flaw to receive products/services without paying the full amount. - Merchants may suffer financial losses due to underpaid orders. - Trust in the integrity of the payment process is compromised. ### Patches The issue is fixed in versions: 1.6.2, 1.7.2, 2.0.2 and above. ### Workarounds To resolve the problem in the end application without updating to the newest patches, there is a need to overwrite `PayPalOrderCompleteProcessor` with modified logic: ```php <?php declare(strict_types=1); namespace App\Processor; use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface; final class PayPalOrderCompleteProcessor { public function __construct(private readonly PaymentStateManagerInterface $paymentStateManager) { } public function completePayPalOrder(OrderInterface $order): void { $payment = $order->getLastPayment(PaymentInterface::STATE_PROCESSING); if ($payment === null) { return; } /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $payment->getMethod(); /** @var GatewayConfigInterface $gatewayConfig */ $gatewayConfig = $paymentMethod->getGatewayConfig(); if ($gatewayConfig->getFactoryName() !== 'sylius.pay_pal') { return; } try { $this->verify($payment); } catch (\Exception) { $this->paymentStateManager->cancel($payment); return; } $this->paymentStateManager->complete($payment); } private function verify(PaymentInterface $payment): void { $totalAmount = $this->getTotalPaymentAmountFromPaypal($payment); if ($payment->getOrder()->getTotal() !== $totalAmount) { throw new \Exception(); } } private function getTotalPaymentAmountFromPaypal(PaymentInterface $payment): int { $details = $payment->getDetails(); return $details['payment_amount'] ?? 0; } } ``` ### IMPORTANT For `PayPalPlugin 2.x` change: ```php $gatewayConfig->getFactoryName() !== 'sylius.pay_pal' ``` to ```php $gatewayConfig->getFactoryName() !== SyliusPayPalExtension::PAYPAL_FACTORY_NAME ``` Also there is a need to overwrite `CompletePayPalOrderListener` with modified logic: ```php <?php declare(strict_types=1); namespace App\EventListener\Workflow; use App\Processor\PayPalOrderCompleteProcessor; use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\Workflow\Event\CompletedEvent; use Webmozart\Assert\Assert; final class CompletePayPalOrderListener { public function __construct(private readonly PayPalOrderCompleteProcessor $completeProcessor) { } public function __invoke(CompletedEvent $event): void { /** @var OrderInterface $order */ $order = $event->getSubject(); Assert::isInstanceOf($order, OrderInterface::class); $this->completeProcessor->completePayPalOrder($order); } } ``` And to overwrite `CaptureAction` with modified logic (if you didn't have it already): ```php <?php declare(strict_types=1); namespace App\Payum\Action; use Payum\Core\Action\ActionInterface; use Payum\Core\Exception\RequestNotSupportedException; use Payum\Core\Request\Capture; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface; use Sylius\PayPalPlugin\Api\CreateOrderApiInterface; use Sylius\PayPalPlugin\Payum\Action\StatusAction; use Sylius\PayPalPlugin\Provider\UuidProviderInterface; final class CaptureAction implements ActionInterface { public function __construct( private CacheAuthorizeClientApiInterface $authorizeClientApi, private CreateOrderApiInterface $createOrderApi, private UuidProviderInterface $uuidProvider, ) { } /** @param Capture $request */ public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); /** @var PaymentInterface $payment */ $payment = $request->getModel(); /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $payment->getMethod(); $token = $this->authorizeClientApi->authorize($paymentMethod); $referenceId = $this->uuidProvider->provide(); $content = $this->createOrderApi->create($token, $payment, $referenceId); if ($content['status'] === 'CREATED') { $payment->setDetails([ 'status' => StatusAction::STATUS_CAPTURED, 'paypal_order_id' => $content['id'], 'reference_id' => $referenceId, 'payment_amount' => $payment->getAmount(), ]); } } public function supports($request): bool { return $request instanceof Capture && $request->getModel() instanceof PaymentInterface ; } } ``` After that, register services in the container when using PayPal 1.x: ```yaml Sylius\PayPalPlugin\EventListener\Workflow\CompletePayPalOrderListener: class: App\EventListener\Workflow\CompletePayPalOrderListener public: true arguments: - '@Sylius\PayPalPlugin\Processor\PayPalOrderCompleteProcessor' tags: - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 } Sylius\PayPalPlugin\Processor\PayPalOrderCompleteProcessor: class: App\Processor\PayPalOrderCompleteProcessor public: true arguments: - '@Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface' Sylius\PayPalPlugin\Payum\Action\CaptureAction: class: App\Payum\Action\CaptureAction public: true arguments: - '@Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface' - '@Sylius\PayPalPlugin\Api\CreateOrderApiInterface' - '@Sylius\PayPalPlugin\Provider\UuidProviderInterface' tags: - { name: 'payum.action', factory: 'sylius.pay_pal', alias: 'payum.action.capture' } ``` or when using PayPal 2.x: ```yaml sylius_paypal.listener.workflow.complete_paypal_order: class: App\EventListener\Workflow\CompletePayPalOrderListener public: true arguments: - '@sylius_paypal.processor.paypal_order_complete' tags: - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 } sylius_paypal.processor.paypal_order_complete: class: App\Processor\PayPalOrderCompleteProcessor public: true arguments: - '@sylius_paypal.manager.payment_state' sylius_paypal.payum.action.capture: class: App\Payum\Action\CaptureAction public: true arguments: - '@sylius_paypal.api.cache_authorize_client' - '@sylius_paypal.api.create_order' - '@sylius_paypal.provider.uuid' tags: - { name: 'payum.action', factory: 'sylius.paypal', alias: 'payum.action.capture' } ``` ### For more information If you have any questions or comments about this advisory: * Open an issue in [Sylius issues](https://github.com/Sylius/Sylius/issues) * Email us at security@sylius.com
A discovered vulnerability allows users to modify their shopping cart after completing the PayPal Checkout process and payment authorization. If a user initiates a PayPal transaction from a product page or the cart page and then returns to the order summary page, they can still manipulate the cart contents before finalizing the order. As a result, the order amount in Sylius may be higher than the amount actually captured by PayPal, leading to a scenario where merchants deliver products or services without full payment. ### Impact - Users can exploit this flaw to receive products/services without paying the full amount. - Merchants may suffer financial losses due to underpaid orders. - Trust in the integrity of the payment process is compromised. ### Patches The issue is fixed in versions: 1.6.2, 1.7.2, 2.0.2 and above. ### Workarounds To resolve the problem in the end application without updating to the newest patches, there is a need to overwrite `PayPalOrderCompleteProcessor` with modified logic: ```php <?php declare(strict_types=1); namespace App\Processor; use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface; final class PayPalOrderCompleteProcessor { public function __construct(private readonly PaymentStateManagerInterface $paymentStateManager) { } public function completePayPalOrder(OrderInterface $order): void { $payment = $order->getLastPayment(PaymentInterface::STATE_PROCESSING); if ($payment === null) { return; } /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $payment->getMethod(); /** @var GatewayConfigInterface $gatewayConfig */ $gatewayConfig = $paymentMethod->getGatewayConfig(); if ($gatewayConfig->getFactoryName() !== 'sylius.pay_pal') { return; } try { $this->verify($payment); } catch (\Exception) { $this->paymentStateManager->cancel($payment); return; } $this->paymentStateManager->complete($payment); } private function verify(PaymentInterface $payment): void { $totalAmount = $this->getTotalPaymentAmountFromPaypal($payment); if ($payment->getOrder()->getTotal() !== $totalAmount) { throw new \Exception(); } } private function getTotalPaymentAmountFromPaypal(PaymentInterface $payment): int { $details = $payment->getDetails(); return $details['payment_amount'] ?? 0; } } ``` ### IMPORTANT For `PayPalPlugin 2.x` change: ```php $gatewayConfig->getFactoryName() !== 'sylius.pay_pal' ``` to ```php $gatewayConfig->getFactoryName() !== SyliusPayPalExtension::PAYPAL_FACTORY_NAME ``` Also there is a need to overwrite `CompletePayPalOrderListener` with modified logic: ```php <?php declare(strict_types=1); namespace App\EventListener\Workflow; use App\Processor\PayPalOrderCompleteProcessor; use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\Workflow\Event\CompletedEvent; use Webmozart\Assert\Assert; final class CompletePayPalOrderListener { public function __construct(private readonly PayPalOrderCompleteProcessor $completeProcessor) { } public function __invoke(CompletedEvent $event): void { /** @var OrderInterface $order */ $order = $event->getSubject(); Assert::isInstanceOf($order, OrderInterface::class); $this->completeProcessor->completePayPalOrder($order); } } ``` And to overwrite `CaptureAction` with modified logic (if you didn't have it already): ```php <?php declare(strict_types=1); namespace App\Payum\Action; use Payum\Core\Action\ActionInterface; use Payum\Core\Exception\RequestNotSupportedException; use Payum\Core\Request\Capture; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface; use Sylius\PayPalPlugin\Api\CreateOrderApiInterface; use Sylius\PayPalPlugin\Payum\Action\StatusAction; use Sylius\PayPalPlugin\Provider\UuidProviderInterface; final class CaptureAction implements ActionInterface { public function __construct( private CacheAuthorizeClientApiInterface $authorizeClientApi, private CreateOrderApiInterface $createOrderApi, private UuidProviderInterface $uuidProvider, ) { } /** @param Capture $request */ public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); /** @var PaymentInterface $payment */ $payment = $request->getModel(); /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $payment->getMethod(); $token = $this->authorizeClientApi->authorize($paymentMethod); $referenceId = $this->uuidProvider->provide(); $content = $this->createOrderApi->create($token, $payment, $referenceId); if ($content['status'] === 'CREATED') { $payment->setDetails([ 'status' => StatusAction::STATUS_CAPTURED, 'paypal_order_id' => $content['id'], 'reference_id' => $referenceId, 'payment_amount' => $payment->getAmount(), ]); } } public function supports($request): bool { return $request instanceof Capture && $request->getModel() instanceof PaymentInterface ; } } ``` After that, register services in the container when using PayPal 1.x: ```yaml Sylius\PayPalPlugin\EventListener\Workflow\CompletePayPalOrderListener: class: App\EventListener\Workflow\CompletePayPalOrderListener public: true arguments: - '@Sylius\PayPalPlugin\Processor\PayPalOrderCompleteProcessor' tags: - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 } Sylius\PayPalPlugin\Processor\PayPalOrderCompleteProcessor: class: App\Processor\PayPalOrderCompleteProcessor public: true arguments: - '@Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface' Sylius\PayPalPlugin\Payum\Action\CaptureAction: class: App\Payum\Action\CaptureAction public: true arguments: - '@Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface' - '@Sylius\PayPalPlugin\Api\CreateOrderApiInterface' - '@Sylius\PayPalPlugin\Provider\UuidProviderInterface' tags: - { name: 'payum.action', factory: 'sylius.pay_pal', alias: 'payum.action.capture' } ``` or when using PayPal 2.x: ```yaml sylius_paypal.listener.workflow.complete_paypal_order: class: App\EventListener\Workflow\CompletePayPalOrderListener public: true arguments: - '@sylius_paypal.processor.paypal_order_complete' tags: - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 } sylius_paypal.processor.paypal_order_complete: class: App\Processor\PayPalOrderCompleteProcessor public: true arguments: - '@sylius_paypal.manager.payment_state' sylius_paypal.payum.action.capture: class: App\Payum\Action\CaptureAction public: true arguments: - '@sylius_paypal.api.cache_authorize_client' - '@sylius_paypal.api.create_order' - '@sylius_paypal.provider.uuid' tags: - { name: 'payum.action', factory: 'sylius.paypal', alias: 'payum.action.capture' } ``` ### For more information If you have any questions or comments about this advisory: * Open an issue in [Sylius issues](https://github.com/Sylius/Sylius/issues) * Email us at security@sylius.com
El Syliud PayPal es el complemento del equipo principal de Sylius para la plataforma PayPal Commerce. En versiones anteriores a las 1.6.2, 1.7.2 y 2.0.2, se descubrió una vulnerabilidad que permitía a los usuarios modificar su carrito de compra tras completar el proceso de pago y la autorización del pago. Si un usuario inicia una transacción de PayPal desde la página de un producto o del carrito y luego regresa a la página de resumen del pedido, aún puede manipular el contenido del carrito antes de finalizarlo. Como resultado, el importe del pedido en Sylius puede ser superior al importe real capturado por PayPal, lo que provoca que los comerciantes entreguen productos o servicios sin el pago completo. El problema se ha solucionado en las versiones 1.6.2, 1.7.2, 2.0.2 y posteriores.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 3.1 | Primary | cve.org | 6.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N |
| 3.1 | Primary | cve.org | 6.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N |
| 3.1 | Secondary | NVD | 6.5 | 2.8 | 3.6 | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N |
| 3.1 | Secondary | GHSA | 6.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N |