GHSA-2xc6-348p-c2x6High

Sylius affected by IDOR in Cart and Checkout LiveComponents

Published
March 11, 2026
Last Modified
March 11, 2026

🔗 CVE IDs covered (1)

📋 Description

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 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.

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:

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

declare(strict_types=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::SYLIUS_SHOP_ADDRESS_UPDATED)]
    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

declare(strict_types=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::SYLIUS_SHOP_CART_CHANGED)]
    #[LiveListener(FormComponent::SYLIUS_SHOP_CART_CLEARED)]
    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

declare(strict_types=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::SYLIUS_SHOP_CART_CHANGED)]
    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:

    sylius_shop.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.shop_user'
            $addressRepository: '@sylius.repository.address'
        tags:
            - { name: 'sylius.live_component.shop', key: 'sylius_shop:checkout:address:form' }

    sylius_shop.twig.component.cart.widget:
        class: App\Twig\Components\Cart\WidgetComponent
        arguments:
            $cartContext: '@sylius.context.cart.composite'
            $orderRepository: '@sylius.repository.order'
        tags:
            - { name: 'sylius.live_component.shop', key: 'sylius_shop:cart:widget' }

    sylius_shop.twig.component.cart.summary:
        class: App\Twig\Components\Cart\SummaryComponent
        arguments:
            $orderRepository: '@sylius.repository.order'
        tags:
            - { name: 'sylius.live_component.shop', key: 'sylius_shop:cart:summary' }

Step 6. Clear cache

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:

For more information

If you have any questions or comments about this advisory:

🎯 Affected products3

  • composer/sylius/sylius:>= 2.0.0, <= 2.0.15
  • composer/sylius/sylius:>= 2.1.0, <= 2.1.11
  • composer/sylius/sylius:>= 2.2.0, <= 2.2.2

🔗 References (3)