Skip to content

Commit 8cb1b4a

Browse files
1 parent 2ba26f7 commit 8cb1b4a

5 files changed

Lines changed: 567 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-2xc6-348p-c2x6",
4+
"modified": "2026-03-11T00:12:47Z",
5+
"published": "2026-03-11T00:12:47Z",
6+
"aliases": [
7+
"CVE-2026-31820"
8+
],
9+
"summary": "Sylius affected by IDOR in Cart and Checkout LiveComponents",
10+
"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 `->find()` 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<?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<OrderInterface> */\n use ResourceFormComponentTrait;\n use TemplatePropTrait;\n\n #[LiveProp]\n public bool $emailExists = false;\n\n /**\n * @param OrderRepositoryInterface<OrderInterface> $repository\n * @param UserRepositoryInterface<ShopUserInterface> $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->initialize($repository, $formFactory, $resourceClass, $formClass);\n }\n\n #[PreReRender(priority: -100)]\n public function checkEmailExist(): void\n {\n $email = $this->formValues['customer']['email'] ?? null;\n if (null !== $email) {\n $this->emailExists = $this->shopUserRepository->findOneByEmail($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->customerContext->getCustomer();\n if (null === $customer) {\n return;\n }\n\n // Fix: findOneByCustomer instead of find — validates ownership\n $address = $this->addressRepository->findOneByCustomer((string) $addressId, $customer);\n if (null === $address) {\n return;\n }\n\n $newAddress = [];\n $newAddress['firstName'] = $address->getFirstName();\n $newAddress['lastName'] = $address->getLastName();\n $newAddress['phoneNumber'] = $address->getPhoneNumber();\n $newAddress['company'] = $address->getCompany();\n $newAddress['countryCode'] = $address->getCountryCode();\n if ($address->getProvinceCode() !== null) {\n $newAddress['provinceCode'] = $address->getProvinceCode();\n }\n if ($address->getProvinceName() !== null) {\n $newAddress['provinceName'] = $address->getProvinceName();\n }\n $newAddress['street'] = $address->getStreet();\n $newAddress['city'] = $address->getCity();\n $newAddress['postcode'] = $address->getPostcode();\n\n $this->formValues[$field] = $newAddress;\n }\n\n protected function instantiateForm(): FormInterface\n {\n return $this->formFactory->create(\n $this->formClass,\n $this->resource,\n ['customer' => $this->customerContext->getCustomer()],\n );\n }\n}\n```\n\n#### Step 3. Override cart WidgetComponent\n\nCreate `src/Twig/Components/Cart/WidgetComponent.php`:\n\n```php\n<?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<OrderInterface> */\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->initialize($orderRepository);\n }\n\n #[PreMount]\n public function initializeCart(): void\n {\n $this->cart = $this->getCart();\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->cart = $this->getCart();\n }\n\n private function getCart(): ?OrderInterface\n {\n try {\n return $this->cartContext->getCart();\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<?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<OrderInterface> */\n use ResourceLivePropTrait;\n use TemplatePropTrait;\n\n #[LiveProp(hydrateWith: 'hydrateResource', dehydrateWith: 'dehydrateResource')]\n public ?ResourceInterface $cart = null;\n\n /** @param OrderRepositoryInterface<OrderInterface> $orderRepository */\n public function __construct(OrderRepositoryInterface $orderRepository)\n {\n $this->initialize($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->cart === null) {\n return;\n }\n\n $this->cart = $this->hydrateResource($this->cart->getId());\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)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"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"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "sylius/sylius"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "2.0.0"
29+
},
30+
{
31+
"fixed": "2.0.16"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 2.0.15"
38+
}
39+
},
40+
{
41+
"package": {
42+
"ecosystem": "Packagist",
43+
"name": "sylius/sylius"
44+
},
45+
"ranges": [
46+
{
47+
"type": "ECOSYSTEM",
48+
"events": [
49+
{
50+
"introduced": "2.1.0"
51+
},
52+
{
53+
"fixed": "2.1.12"
54+
}
55+
]
56+
}
57+
],
58+
"database_specific": {
59+
"last_known_affected_version_range": "<= 2.1.11"
60+
}
61+
},
62+
{
63+
"package": {
64+
"ecosystem": "Packagist",
65+
"name": "sylius/sylius"
66+
},
67+
"ranges": [
68+
{
69+
"type": "ECOSYSTEM",
70+
"events": [
71+
{
72+
"introduced": "2.2.0"
73+
},
74+
{
75+
"fixed": "2.2.3"
76+
}
77+
]
78+
}
79+
],
80+
"database_specific": {
81+
"last_known_affected_version_range": "<= 2.2.2"
82+
}
83+
}
84+
],
85+
"references": [
86+
{
87+
"type": "WEB",
88+
"url": "https://github.com/Sylius/Sylius/security/advisories/GHSA-2xc6-348p-c2x6"
89+
},
90+
{
91+
"type": "PACKAGE",
92+
"url": "https://github.com/Sylius/Sylius"
93+
}
94+
],
95+
"database_specific": {
96+
"cwe_ids": [
97+
"CWE-639"
98+
],
99+
"severity": "HIGH",
100+
"github_reviewed": true,
101+
"github_reviewed_at": "2026-03-11T00:12:47Z",
102+
"nvd_published_at": null
103+
}
104+
}

0 commit comments

Comments
 (0)