{"id":"GHSA-2xc6-348p-c2x6","summary":"Sylius affected by IDOR in Cart and Checkout LiveComponents","details":"### Impact\nAn authenticated Insecure Direct Object Reference (IDOR) vulnerability exists in multiple shop LiveComponents due to unvalidated resource IDs accepted via `#[LiveArg]` parameters. Unlike props, which are protected by LiveComponent's `@checksum`, `args` are fully user-controlled - any action that accepts a resource ID via `#[LiveArg]` and loads it with `-\u003efind()` without ownership validation is vulnerable.\n\nCheckout address **FormComponent** (`addressFieldUpdated` action): Accepts an `addressId` via `#[LiveArg]` and loads it without verifying ownership, exposing another user's first name, last name, company, phone number, street, city, postcode, and country.\n\nCart **WidgetComponent** (`refreshCart` action): Accepts a `cartId` via `#[LiveArg]` and loads any order directly from the repository, exposing order total and item count.\n\nCart **SummaryComponent** (`refreshCart` action): Accepts a `cartId` via `#[LiveArg]` and loads any order directly from the repository, exposing subtotal, discount, shipping cost, taxes (excluded and included), and order total.\n\nSince `sylius_order` contains both active carts (`state=cart`) and completed orders (`state=new/fulfilled`) in the same ID space, the cart IDOR exposes data from all orders, not just active carts.\n\n### Patches\nThe issue is fixed in versions: 2.0.16, 2.1.12, 2.2.3 and above.\n\n### Workarounds\n\nOverride vulnerable LiveComponent classes at the project level to add authorization checks to `#[LiveArg]` parameters.\n\n#### Step 1. Exclude component overrides from default autowiring\n\nIn `config/services.yaml`, add `Twig/Component` to the exclude list to prevent duplicate service registration:\n\n```yaml\nApp\\:\n    resource: '../src/*'\n    exclude: '../src/{Entity,Kernel.php,Twig/Components}'\n```\n\n#### Step 2. Override checkout address FormComponent\n\nCreate `src/Twig/Components/Checkout/Address/FormComponent.php`:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Twig\\Components\\Checkout\\Address;\n\nuse Sylius\\Bundle\\ShopBundle\\Twig\\Component\\Checkout\\Address\\AddressBookComponent;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\ResourceFormComponentTrait;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\TemplatePropTrait;\nuse Sylius\\Component\\Core\\Model\\OrderInterface;\nuse Sylius\\Component\\Core\\Model\\ShopUserInterface;\nuse Sylius\\Component\\Core\\Repository\\AddressRepositoryInterface;\nuse Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface;\nuse Sylius\\Component\\Customer\\Context\\CustomerContextInterface;\nuse Sylius\\Component\\User\\Repository\\UserRepositoryInterface;\nuse Symfony\\Component\\Form\\FormFactoryInterface;\nuse Symfony\\Component\\Form\\FormInterface;\nuse Symfony\\UX\\LiveComponent\\Attribute\\AsLiveComponent;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveArg;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveListener;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveProp;\nuse Symfony\\UX\\LiveComponent\\Attribute\\PreReRender;\n\n#[AsLiveComponent]\nclass FormComponent\n{\n    /** @use ResourceFormComponentTrait\u003cOrderInterface\u003e */\n    use ResourceFormComponentTrait;\n    use TemplatePropTrait;\n\n    #[LiveProp]\n    public bool $emailExists = false;\n\n    /**\n     * @param OrderRepositoryInterface\u003cOrderInterface\u003e $repository\n     * @param UserRepositoryInterface\u003cShopUserInterface\u003e $shopUserRepository\n     */\n    public function __construct(\n        OrderRepositoryInterface $repository,\n        FormFactoryInterface $formFactory,\n        string $resourceClass,\n        string $formClass,\n        protected readonly CustomerContextInterface $customerContext,\n        protected readonly UserRepositoryInterface $shopUserRepository,\n        protected readonly AddressRepositoryInterface $addressRepository,\n    ) {\n        $this-\u003einitialize($repository, $formFactory, $resourceClass, $formClass);\n    }\n\n    #[PreReRender(priority: -100)]\n    public function checkEmailExist(): void\n    {\n        $email = $this-\u003eformValues['customer']['email'] ?? null;\n        if (null !== $email) {\n            $this-\u003eemailExists = $this-\u003eshopUserRepository-\u003efindOneByEmail($email) !== null;\n        }\n    }\n\n    #[LiveListener(AddressBookComponent::SYLIUS_SHOP_ADDRESS_UPDATED)]\n    public function addressFieldUpdated(#[LiveArg] mixed $addressId, #[LiveArg] string $field): void\n    {\n        $customer = $this-\u003ecustomerContext-\u003egetCustomer();\n        if (null === $customer) {\n            return;\n        }\n\n        // Fix: findOneByCustomer instead of find — validates ownership\n        $address = $this-\u003eaddressRepository-\u003efindOneByCustomer((string) $addressId, $customer);\n        if (null === $address) {\n            return;\n        }\n\n        $newAddress = [];\n        $newAddress['firstName'] = $address-\u003egetFirstName();\n        $newAddress['lastName'] = $address-\u003egetLastName();\n        $newAddress['phoneNumber'] = $address-\u003egetPhoneNumber();\n        $newAddress['company'] = $address-\u003egetCompany();\n        $newAddress['countryCode'] = $address-\u003egetCountryCode();\n        if ($address-\u003egetProvinceCode() !== null) {\n            $newAddress['provinceCode'] = $address-\u003egetProvinceCode();\n        }\n        if ($address-\u003egetProvinceName() !== null) {\n            $newAddress['provinceName'] = $address-\u003egetProvinceName();\n        }\n        $newAddress['street'] = $address-\u003egetStreet();\n        $newAddress['city'] = $address-\u003egetCity();\n        $newAddress['postcode'] = $address-\u003egetPostcode();\n\n        $this-\u003eformValues[$field] = $newAddress;\n    }\n\n    protected function instantiateForm(): FormInterface\n    {\n        return $this-\u003eformFactory-\u003ecreate(\n            $this-\u003eformClass,\n            $this-\u003eresource,\n            ['customer' =\u003e $this-\u003ecustomerContext-\u003egetCustomer()],\n        );\n    }\n}\n```\n\n#### Step 3. Override cart WidgetComponent\n\nCreate `src/Twig/Components/Cart/WidgetComponent.php`:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Twig\\Components\\Cart;\n\nuse Sylius\\Bundle\\ShopBundle\\Twig\\Component\\Cart\\FormComponent;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\ResourceLivePropTrait;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\TemplatePropTrait;\nuse Sylius\\Component\\Core\\Model\\OrderInterface;\nuse Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface;\nuse Sylius\\Component\\Order\\Context\\CartContextInterface;\nuse Sylius\\Component\\Order\\Context\\CartNotFoundException;\nuse Sylius\\Resource\\Model\\ResourceInterface;\nuse Sylius\\TwigHooks\\LiveComponent\\HookableLiveComponentTrait;\nuse Symfony\\UX\\LiveComponent\\Attribute\\AsLiveComponent;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveArg;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveListener;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveProp;\nuse Symfony\\UX\\LiveComponent\\DefaultActionTrait;\nuse Symfony\\UX\\TwigComponent\\Attribute\\PreMount;\n\n#[AsLiveComponent]\nclass WidgetComponent\n{\n    use DefaultActionTrait;\n    use HookableLiveComponentTrait;\n    use TemplatePropTrait;\n\n    /** @use ResourceLivePropTrait\u003cOrderInterface\u003e */\n    use ResourceLivePropTrait;\n\n    #[LiveProp(hydrateWith: 'hydrateResource', dehydrateWith: 'dehydrateResource')]\n    public ?ResourceInterface $cart = null;\n\n    public function __construct(\n        protected readonly CartContextInterface $cartContext,\n        OrderRepositoryInterface $orderRepository,\n    ) {\n        $this-\u003einitialize($orderRepository);\n    }\n\n    #[PreMount]\n    public function initializeCart(): void\n    {\n        $this-\u003ecart = $this-\u003egetCart();\n    }\n\n    #[LiveListener(FormComponent::SYLIUS_SHOP_CART_CHANGED)]\n    #[LiveListener(FormComponent::SYLIUS_SHOP_CART_CLEARED)]\n    public function refreshCart(#[LiveArg] mixed $cartId = null): void\n    {\n        // Fix: ignore user-supplied cartId, always load from session\n        $this-\u003ecart = $this-\u003egetCart();\n    }\n\n    private function getCart(): ?OrderInterface\n    {\n        try {\n            return $this-\u003ecartContext-\u003egetCart();\n        } catch (CartNotFoundException) {\n            return null;\n        }\n\n       return $cart;\n    }\n}\n```\n\n#### Step 4. Override cart SummaryComponent\n\nCreate `src/Twig/Components/Cart/SummaryComponent.php`:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Twig\\Components\\Cart;\n\nuse Sylius\\Bundle\\ShopBundle\\Twig\\Component\\Cart\\FormComponent;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\ResourceLivePropTrait;\nuse Sylius\\Bundle\\UiBundle\\Twig\\Component\\TemplatePropTrait;\nuse Sylius\\Component\\Core\\Model\\OrderInterface;\nuse Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface;\nuse Sylius\\Resource\\Model\\ResourceInterface;\nuse Sylius\\TwigHooks\\LiveComponent\\HookableLiveComponentTrait;\nuse Symfony\\UX\\LiveComponent\\Attribute\\AsLiveComponent;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveArg;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveListener;\nuse Symfony\\UX\\LiveComponent\\Attribute\\LiveProp;\nuse Symfony\\UX\\LiveComponent\\DefaultActionTrait;\n\n#[AsLiveComponent]\nclass SummaryComponent\n{\n    use DefaultActionTrait;\n    use HookableLiveComponentTrait;\n\n    /** @use ResourceLivePropTrait\u003cOrderInterface\u003e */\n    use ResourceLivePropTrait;\n    use TemplatePropTrait;\n\n    #[LiveProp(hydrateWith: 'hydrateResource', dehydrateWith: 'dehydrateResource')]\n    public ?ResourceInterface $cart = null;\n\n    /** @param OrderRepositoryInterface\u003cOrderInterface\u003e $orderRepository */\n    public function __construct(OrderRepositoryInterface $orderRepository)\n    {\n        $this-\u003einitialize($orderRepository);\n    }\n\n    #[LiveListener(FormComponent::SYLIUS_SHOP_CART_CHANGED)]\n    public function refreshCart(#[LiveArg] mixed $cartId): void\n    {\n        // Fix: ignore user-supplied cartId, reload from checksummed cart prop\n        if ($this-\u003ecart === null) {\n            return;\n        }\n\n        $this-\u003ecart = $this-\u003ehydrateResource($this-\u003ecart-\u003egetId());\n    }\n}\n```\n\n#### Step 5. Register overridden services\n\nIn `config/services.yaml`, add:\n\n```yaml\n    sylius_shop.twig.component.checkout.address.form:\n        class: App\\Twig\\Components\\Checkout\\Address\\FormComponent\n        arguments:\n            $repository: '@sylius.repository.order'\n            $formFactory: '@form.factory'\n            $resourceClass: '%sylius.model.order.class%'\n            $formClass: 'Sylius\\Bundle\\ShopBundle\\Form\\Type\\Checkout\\AddressType'\n            $customerContext: '@sylius.context.customer'\n            $shopUserRepository: '@sylius.repository.shop_user'\n            $addressRepository: '@sylius.repository.address'\n        tags:\n            - { name: 'sylius.live_component.shop', key: 'sylius_shop:checkout:address:form' }\n\n    sylius_shop.twig.component.cart.widget:\n        class: App\\Twig\\Components\\Cart\\WidgetComponent\n        arguments:\n            $cartContext: '@sylius.context.cart.composite'\n            $orderRepository: '@sylius.repository.order'\n        tags:\n            - { name: 'sylius.live_component.shop', key: 'sylius_shop:cart:widget' }\n\n    sylius_shop.twig.component.cart.summary:\n        class: App\\Twig\\Components\\Cart\\SummaryComponent\n        arguments:\n            $orderRepository: '@sylius.repository.order'\n        tags:\n            - { name: 'sylius.live_component.shop', key: 'sylius_shop:cart:summary' }\n```\n\n#### Step 6. Clear cache\n\n```bash\nphp bin/console cache:clear\n```\n\n### Reporters\n\nWe would like to extend our gratitude to the following individuals for their detailed reporting and responsible disclosure of this vulnerability:\n- Peter Stöckli (@p-)\n- Man Yue Mo (@m-y-mo)\n- The [GitHub Security Lab](https://securitylab.github.com) team\n\n### For more information\nIf you have any questions or comments about this advisory:\n\n- Open an issue in [Sylius issues](https://github.com/Sylius/Sylius/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen)\n- Email us at [security@sylius.com](mailto:security@sylius.com)","aliases":["CVE-2026-31820"],"modified":"2026-03-13T05:56:21.460164Z","published":"2026-03-11T00:12:47Z","database_specific":{"cwe_ids":["CWE-639"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-11T00:12:47Z","nvd_published_at":"2026-03-10T22:16:19Z"},"references":[{"type":"WEB","url":"https://github.com/Sylius/Sylius/security/advisories/GHSA-2xc6-348p-c2x6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-31820"},{"type":"PACKAGE","url":"https://github.com/Sylius/Sylius"}],"affected":[{"package":{"name":"sylius/sylius","ecosystem":"Packagist","purl":"pkg:composer/sylius/sylius"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.0.16"}]}],"versions":["v2.0.0","v2.0.1","v2.0.10","v2.0.11","v2.0.12","v2.0.13","v2.0.14","v2.0.15","v2.0.2","v2.0.3","v2.0.4","v2.0.5","v2.0.6","v2.0.7","v2.0.8","v2.0.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.0.15","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-2xc6-348p-c2x6/GHSA-2xc6-348p-c2x6.json"}},{"package":{"name":"sylius/sylius","ecosystem":"Packagist","purl":"pkg:composer/sylius/sylius"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.1.0"},{"fixed":"2.1.12"}]}],"versions":["v2.1.0","v2.1.1","v2.1.10","v2.1.11","v2.1.2","v2.1.3","v2.1.4","v2.1.5","v2.1.6","v2.1.7","v2.1.8","v2.1.9"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-2xc6-348p-c2x6/GHSA-2xc6-348p-c2x6.json","last_known_affected_version_range":"\u003c= 2.1.11"}},{"package":{"name":"sylius/sylius","ecosystem":"Packagist","purl":"pkg:composer/sylius/sylius"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.2.0"},{"fixed":"2.2.3"}]}],"versions":["v2.2.0","v2.2.1","v2.2.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-2xc6-348p-c2x6/GHSA-2xc6-348p-c2x6.json","last_known_affected_version_range":"\u003c= 2.2.2"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"}]}