CVE-2026-31820: Sylius affected by IDOR in Cart and Checkout LiveComponents
Impact An 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.
Checkout 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.
Cart WidgetComponent (refreshCart action): Accepts a cartId via #[LiveArg] and loads any order directly from the repository, exposing order total and item count.
Cart 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.
Since syliusorder 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.
Patches The issue is fixed in versions: 2.0.16, 2.1.12, 2.2.3 and above.
Workarounds
Override vulnerable LiveComponent classes at the project level to add authorization checks to #[LiveArg] parameters.
Step 1. Exclude component overrides from default autowiring
In config/services.yaml, add Twig/Component to the exclude list to prevent duplicate service registration:
yaml App\: resource: '../src/' exclude: '../src/{Entity,Kernel.php,Twig/Components}'
Step 2. Override checkout address FormComponent
Create src/Twig/Components/Checkout/Address/FormComponent.php:
php <?php
declare(stricttypes=1);
namespace App\Twig\Components\Checkout\Address;
use Sylius\Bundle\ShopBundle\Twig\Component\Checkout\Address\AddressBookComponent; use Sylius\Bundle\UiBundle\Twig\Component\ResourceFormComponentTrait; use Sylius\Bundle\UiBundle\Twig\Component\TemplatePropTrait; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\AddressRepositoryInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Customer\Context\CustomerContextInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\Attribute\PreReRender;
#[AsLiveComponent] class FormComponent { / @use ResourceFormComponentTrait<OrderInterface> / use ResourceFormComponentTrait; use TemplatePropTrait;
#[LiveProp] public bool $emailExists = false;
/ @param OrderRepositoryInterface<OrderInterface> $repository @param UserRepositoryInterface<ShopUserInterface> $shopUserRepository / public function construct( OrderRepositoryInterface $repository, FormFactoryInterface $formFactory, string $resourceClass, string $formClass, protected readonly CustomerContextInterface $customerContext, protected readonly UserRepositoryInterface $shopUserRepository, protected readonly AddressRepositoryInterface $addressRepository, ) { $this->initialize($repository, $formFactory, $resourceClass, $formClass); }
#[PreReRender(priority: -100)] public function checkEmailExist(): void { $email = $this->formValues['customer']['email'] ?? null; if (null !== $email) { $this->emailExists = $this->shopUserRepository->findOneByEmail($email) !== null; } }
#[LiveListener(AddressBookComponent::SYLIUSSHOPADDRESSUPDATED)] public function addressFieldUpdated(#[LiveArg] mixed $addressId, #[LiveArg] string $field): void { $customer = $this->customerContext->getCustomer(); if (null === $customer) { return; }
// Fix: findOneByCustomer instead of find — validates ownership $address = $this->addressRepository->findOneByCustomer((string) $addressId, $customer); if (null === $address) { return; }
$newAddress = []; $newAddress['firstName'] = $address->getFirstName(); $newAddress['lastName'] = $address->getLastName(); $newAddress['phoneNumber'] = $address->getPhoneNumber(); $newAddress['company'] = $address->getCompany(); $newAddress['countryCode'] = $address->getCountryCode(); if ($address->getProvinceCode() !== null) { $newAddress['provinceCode'] = $address->getProvinceCode(); } if ($address->getProvinceName() !== null) { $newAddress['provinceName'] = $address->getProvinceName(); } $newAddress['street'] = $address->getStreet(); $newAddress['city'] = $address->getCity(); $newAddress['postcode'] = $address->getPostcode();
$this->formValues[$field] = $newAddress; }
protected function instantiateForm(): FormInterface { return $this->formFactory->create( $this->formClass, $this->resource, ['customer' => $this->customerContext->getCustomer()], ); } }
Step 3. Override cart WidgetComponent
Create src/Twig/Components/Cart/WidgetComponent.php:
php <?php
declare(stricttypes=1);
namespace App\Twig\Components\Cart;
use Sylius\Bundle\ShopBundle\Twig\Component\Cart\FormComponent; use Sylius\Bundle\UiBundle\Twig\Component\ResourceLivePropTrait; use Sylius\Bundle\UiBundle\Twig\Component\TemplatePropTrait; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Order\Context\CartContextInterface; use Sylius\Component\Order\Context\CartNotFoundException; use Sylius\Resource\Model\ResourceInterface; use Sylius\TwigHooks\LiveComponent\HookableLiveComponentTrait; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\TwigComponent\Attribute\PreMount;
#[AsLiveComponent] class WidgetComponent { use DefaultActionTrait; use HookableLiveComponentTrait; use TemplatePropTrait;
/ @use ResourceLivePropTrait<OrderInterface> / use ResourceLivePropTrait;
#[LiveProp(hydrateWith: 'hydrateResource', dehydrateWith: 'dehydrateResource')] public ?ResourceInterface $cart = null;
public function construct( protected readonly CartContextInterface $cartContext, OrderRepositoryInterface $orderRepository, ) { $this->initialize($orderRepository); }
#[PreMount] public function initializeCart(): void { $this->cart = $this->getCart(); }
#[LiveListener(FormComponent::SYLIUSSHOPCARTCHANGED)] #[LiveListener(FormComponent::SYLIUSSHOPCARTCLEARED)] public function refreshCart(#[LiveArg] mixed $cartId = null): void { // Fix: ignore user-supplied cartId, always load from session $this->cart = $this->getCart(); }
private function getCart(): ?OrderInterface { try { return $this->cartContext->getCart(); } catch (CartNotFoundException) { return null; }
return $cart; } }
Step 4. Override cart SummaryComponent
Create src/Twig/Components/Cart/SummaryComponent.php:
php <?php
declare(stricttypes=1);
namespace App\Twig\Components\Cart;
use Sylius\Bundle\ShopBundle\Twig\Component\Cart\FormComponent; use Sylius\Bundle\UiBundle\Twig\Component\ResourceLivePropTrait; use Sylius\Bundle\UiBundle\Twig\Component\TemplatePropTrait; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Resource\Model\ResourceInterface; use Sylius\TwigHooks\LiveComponent\HookableLiveComponentTrait; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent] class SummaryComponent { use DefaultActionTrait; use HookableLiveComponentTrait;
/ @use ResourceLivePropTrait<OrderInterface> / use ResourceLivePropTrait; use TemplatePropTrait;
#[LiveProp(hydrateWith: 'hydrateResource', dehydrateWith: 'dehydrateResource')] public ?ResourceInterface $cart = null;
/ @param OrderRepositoryInterface<OrderInterface> $orderRepository / public function construct(OrderRepositoryInterface $orderRepository) { $this->initialize($orderRepository); }
#[LiveListener(FormComponent::SYLIUSSHOPCARTCHANGED)] public function refreshCart(#[LiveArg] mixed $cartId): void { // Fix: ignore user-supplied cartId, reload from checksummed cart prop if ($this->cart === null) { return; }
$this->cart = $this->hydrateResource($this->cart->getId()); } }
Step 5. Register overridden services
In config/services.yaml, add:
yaml syliusshop.twig.component.checkout.address.form: class: App\Twig\Components\Checkout\Address\FormComponent arguments: $repository: '@sylius.repository.order' $formFactory: '@form.factory' $resourceClass: '%sylius.model.order.class%' $formClass: 'Sylius\Bundle\ShopBundle\Form\Type\Checkout\AddressType' $customerContext: '@sylius.context.customer' $shopUserRepository: '@sylius.repository.shopuser' $addressRepository: '@sylius.repository.address' tags: - { name: 'sylius.livecomponent.shop', key: 'syliusshop:checkout:address:form' }
syliusshop.twig.component.cart.widget: class: App\Twig\Components\Cart\WidgetComponent arguments: $cartContext: '@sylius.context.cart.composite' $orderRepository: '@sylius.repository.order' tags: - { name: 'sylius.livecomponent.shop', key: 'syliusshop:cart:widget' }
syliusshop.twig.component.cart.summary: class: App\Twig\Components\Cart\SummaryComponent arguments: $orderRepository: '@sylius.repository.order' tags: - { name: 'sylius.livecomponent.shop', key: 'syliusshop:cart:summary' }
Step 6. Clear cache
bash php bin/console cache:clear
Reporters
We would like to extend our gratitude to the following individuals for their detailed reporting and responsible disclosure of this vulnerability: - Peter Stöckli (@p-) - Man Yue Mo (@m-y-mo) - The GitHub Security Lab team
For more information If you have any questions or comments about this advisory:
- Open an issue in Sylius issues - Email us at security@sylius.com
Other sources
Sylius is an Open Source eCommerce Framework on Symfony. An 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. Checkout 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. Cart WidgetComponent (refreshCart action): Accepts a cartId via #[LiveArg] and loads any order directly from the repository, exposing order total and item count. Cart 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. Since syliusorder 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. The issue is fixed in versions: 2.0.16, 2.1.12, 2.2.3 and above.
— MITRE
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-31820?
CVE-2026-31820 is classified as a critical vulnerability due to its exploitation potential in authenticated sessions.
How do I fix CVE-2026-31820?
To remediate CVE-2026-31820, upgrade Sylius to version 2.2.3, 2.1.12, or 2.0.16.
What types of components are affected by CVE-2026-31820?
CVE-2026-31820 affects multiple shop LiveComponents within the Sylius platform.
What is the main issue exposed by CVE-2026-31820?
The main issue in CVE-2026-31820 is an Insecure Direct Object Reference (IDOR) that allows unauthorized access to resources.
Who is vulnerable to CVE-2026-31820?
Any authenticated user of Sylius versions up to 2.2.2, 2.1.11, and 2.0.15 can be exposed to CVE-2026-31820.