{"id":"CVE-2025-30152","aliases":["GHSA-hxg4-65p5-9w37"],"url":"https://o3.security/vulnerability/CVE-2025-30152","summary":"Sylius PayPal Plugin has an Order Manipulation Vulnerability after PayPal Checkout","details":"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.\n\n### Impact\n\n- Users can exploit this flaw to receive products/services without paying the full amount.\n- Merchants may suffer financial losses due to underpaid orders.\n- Trust in the integrity of the payment process is compromised.\n\n### Patches\n\nThe issue is fixed in versions: 1.6.2, 1.7.2, 2.0.2 and above.\n\n### Workarounds\n\nTo resolve the problem in the end application without updating to the newest patches, there is a need to overwrite `PayPalOrderCompleteProcessor` with modified logic:\n\n```php\n<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Processor;\n\nuse Sylius\\Bundle\\PayumBundle\\Model\\GatewayConfigInterface;\nuse Sylius\\Component\\Core\\Model\\OrderInterface;\nuse Sylius\\Component\\Core\\Model\\PaymentInterface;\nuse Sylius\\Component\\Core\\Model\\PaymentMethodInterface;\nuse Sylius\\PayPalPlugin\\Manager\\PaymentStateManagerInterface;\n\nfinal class PayPalOrderCompleteProcessor\n{\n    public function __construct(private readonly PaymentStateManagerInterface $paymentStateManager) {\n    }\n\n    public function completePayPalOrder(OrderInterface $order): void\n    {\n        $payment = $order->getLastPayment(PaymentInterface::STATE_PROCESSING);\n        if ($payment === null) {\n            return;\n        }\n\n        /** @var PaymentMethodInterface $paymentMethod */\n        $paymentMethod = $payment->getMethod();\n        /** @var GatewayConfigInterface $gatewayConfig */\n        $gatewayConfig = $paymentMethod->getGatewayConfig();\n\n        if ($gatewayConfig->getFactoryName() !== 'sylius.pay_pal') {\n            return;\n        }\n\n        try {\n            $this->verify($payment);\n        } catch (\\Exception) {\n            $this->paymentStateManager->cancel($payment);\n\n            return;\n        }\n\n        $this->paymentStateManager->complete($payment);\n    }\n\n    private function verify(PaymentInterface $payment): void\n    {\n        $totalAmount = $this->getTotalPaymentAmountFromPaypal($payment);\n\n        if ($payment->getOrder()->getTotal() !== $totalAmount) {\n            throw new \\Exception();\n        }\n    }\n\n    private function getTotalPaymentAmountFromPaypal(PaymentInterface $payment): int\n    {\n        $details = $payment->getDetails();\n\n        return $details['payment_amount'] ?? 0;\n    }\n}\n```\n\n### IMPORTANT\n\nFor `PayPalPlugin 2.x` change:\n```php\n$gatewayConfig->getFactoryName() !== 'sylius.pay_pal'\n```\nto\n```php\n$gatewayConfig->getFactoryName() !== SyliusPayPalExtension::PAYPAL_FACTORY_NAME\n```\n\nAlso there is a need to overwrite `CompletePayPalOrderListener` with modified logic:\n\n```php\n<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\EventListener\\Workflow;\n\nuse App\\Processor\\PayPalOrderCompleteProcessor;\nuse Sylius\\Component\\Core\\Model\\OrderInterface;\nuse Symfony\\Component\\Workflow\\Event\\CompletedEvent;\nuse Webmozart\\Assert\\Assert;\n\nfinal class CompletePayPalOrderListener\n{\n    public function __construct(private readonly PayPalOrderCompleteProcessor $completeProcessor)\n    {\n    }\n\n    public function __invoke(CompletedEvent $event): void\n    {\n        /** @var OrderInterface $order */\n        $order = $event->getSubject();\n        Assert::isInstanceOf($order, OrderInterface::class);\n\n        $this->completeProcessor->completePayPalOrder($order);\n    }\n}\n\n```\n\nAnd to overwrite `CaptureAction` with modified logic (if you didn't have it already):\n\n```php\n<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Payum\\Action;\n\nuse Payum\\Core\\Action\\ActionInterface;\nuse Payum\\Core\\Exception\\RequestNotSupportedException;\nuse Payum\\Core\\Request\\Capture;\nuse Sylius\\Component\\Core\\Model\\PaymentInterface;\nuse Sylius\\Component\\Core\\Model\\PaymentMethodInterface;\nuse Sylius\\PayPalPlugin\\Api\\CacheAuthorizeClientApiInterface;\nuse Sylius\\PayPalPlugin\\Api\\CreateOrderApiInterface;\nuse Sylius\\PayPalPlugin\\Payum\\Action\\StatusAction;\nuse Sylius\\PayPalPlugin\\Provider\\UuidProviderInterface;\n\nfinal class CaptureAction implements ActionInterface\n{\n    public function __construct(\n        private CacheAuthorizeClientApiInterface $authorizeClientApi,\n        private CreateOrderApiInterface $createOrderApi,\n        private UuidProviderInterface $uuidProvider,\n    ) {\n    }\n\n    /** @param Capture $request */\n    public function execute($request): void\n    {\n        RequestNotSupportedException::assertSupports($this, $request);\n\n        /** @var PaymentInterface $payment */\n        $payment = $request->getModel();\n        /** @var PaymentMethodInterface $paymentMethod */\n        $paymentMethod = $payment->getMethod();\n\n        $token = $this->authorizeClientApi->authorize($paymentMethod);\n\n        $referenceId = $this->uuidProvider->provide();\n        $content = $this->createOrderApi->create($token, $payment, $referenceId);\n\n        if ($content['status'] === 'CREATED') {\n            $payment->setDetails([\n                'status' => StatusAction::STATUS_CAPTURED,\n                'paypal_order_id' => $content['id'],\n                'reference_id' => $referenceId,\n                'payment_amount' => $payment->getAmount(),\n            ]);\n        }\n    }\n\n    public function supports($request): bool\n    {\n        return\n            $request instanceof Capture &&\n            $request->getModel() instanceof PaymentInterface\n        ;\n    }\n}\n```\n\nAfter that, register services in the container when using PayPal 1.x:\n\n```yaml\nSylius\\PayPalPlugin\\EventListener\\Workflow\\CompletePayPalOrderListener:\n    class: App\\EventListener\\Workflow\\CompletePayPalOrderListener\n    public: true\n    arguments:\n        - '@Sylius\\PayPalPlugin\\Processor\\PayPalOrderCompleteProcessor'\n    tags: \n        - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 }\n    \nSylius\\PayPalPlugin\\Processor\\PayPalOrderCompleteProcessor:\n    class: App\\Processor\\PayPalOrderCompleteProcessor\n    public: true\n    arguments:\n        - '@Sylius\\PayPalPlugin\\Manager\\PaymentStateManagerInterface'\n\nSylius\\PayPalPlugin\\Payum\\Action\\CaptureAction:\n    class: App\\Payum\\Action\\CaptureAction\n    public: true\n    arguments:\n        - '@Sylius\\PayPalPlugin\\Api\\CacheAuthorizeClientApiInterface'\n        - '@Sylius\\PayPalPlugin\\Api\\CreateOrderApiInterface'\n        - '@Sylius\\PayPalPlugin\\Provider\\UuidProviderInterface'\n    tags:\n        - { name: 'payum.action', factory: 'sylius.pay_pal', alias: 'payum.action.capture' }\n```\n\nor when using PayPal 2.x:\n\n```yaml\nsylius_paypal.listener.workflow.complete_paypal_order:\n    class: App\\EventListener\\Workflow\\CompletePayPalOrderListener\n    public: true\n    arguments:\n        - '@sylius_paypal.processor.paypal_order_complete'\n    tags: \n        - { name: 'kernel.event_listener', event: 'workflow.sylius_order_checkout.completed.complete', priority: 100 }\n    \nsylius_paypal.processor.paypal_order_complete:\n    class: App\\Processor\\PayPalOrderCompleteProcessor\n    public: true\n    arguments:\n        - '@sylius_paypal.manager.payment_state'\n\nsylius_paypal.payum.action.capture:\n    class: App\\Payum\\Action\\CaptureAction\n    public: true\n    arguments:\n        - '@sylius_paypal.api.cache_authorize_client'\n        - '@sylius_paypal.api.create_order'\n        - '@sylius_paypal.provider.uuid'\n    tags:\n        - { name: 'payum.action', factory: 'sylius.paypal', alias: 'payum.action.capture' }\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [Sylius issues](https://github.com/Sylius/Sylius/issues)\n* Email us at security@sylius.com","published":"2025-03-19T15:57:32.445Z","modified":"2026-08-12T03:51:34.977352797Z","cvss":{"score":6.5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N"},"epss":{"score":0.00337,"percentile":0.27085,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"sylius/paypal-plugin","fixedVersion":"1.6.2"},{"ecosystem":"Packagist","name":"sylius/paypal-plugin","fixedVersion":"1.7.2"},{"ecosystem":"Packagist","name":"sylius/paypal-plugin","fixedVersion":"2.0.2"}],"fix":{"url":"https://github.com/Sylius/PayPalPlugin/commit/5613df827a6d4fc50862229295976200a68e97aa","label":"Sylius/PayPalPlugin@5613df8"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2025/30xxx/CVE-2025-30152.json"},{"type":"ADVISORY","url":"https://github.com/Sylius/PayPalPlugin/security/advisories/GHSA-hxg4-65p5-9w37"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-30152"},{"type":"FIX","url":"https://github.com/Sylius/PayPalPlugin/commit/5613df827a6d4fc50862229295976200a68e97aa"},{"type":"PACKAGE","url":"https://github.com/Sylius/PayPalPlugin"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:34.977352797Z"}}