GitHub Advisoryadvisory · Packagist
GHSA-mr9r-h354-966r Sylius: IDOR on Shop Payment Request API endpoints
MEDIUM
Official page ↗
- Aliases
- CVE-2026-53639
- Ecosystem
- Packagist
- Packages
- sylius/sylius
- Severity
- Moderate
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-639
- Reviewed
- 2026-07-09
Affected packages
• Packagist sylius/sylius: >= 2.0.0, < 2.0.18 → fixed in 2.0.18
• Packagist sylius/sylius: >= 2.1.0, < 2.1.15 → fixed in 2.1.15
• Packagist sylius/sylius: >= 2.2.0, < 2.2.6 → fixed in 2.2.6
Description
Impact
The `GET /api/v2/shop/payment-requests/{hash}` and `PUT /api/v2/shop/payment-requests/{hash}` endpoints look up the payment request solely by the hash from the URL. No ownership check is performed against the authenticated customer or the underlying order.
An attacker who obtains a payment request hash can:
• read the payment request and, through the `payment` IRI in the response, recover the underlying order's `tokenValue` (which itself grants access to the full order, items, addresses, customer email, totals);
• update the payment request payload (e.g. `target_path`, `after_path`). These fields are used by the front-end controller to redirect the user after the payment, so an attacker can flip them to an attacker-controlled URL and intercept the buyer.
The hash is a UUID, so it has to be obtained out-of-band (logs, shared links, referrer headers, a co-located client), but once it is known no other credential is required, neither authentication nor knowledge of the order token.
The creation endpoint `POST /api/v2/shop/orders/{tokenValue}/payment-requests` shares the same flaw: it resolves the target order solely from the `tokenValue` in the URL without verifying that the caller owns the order.
Patches
The issue is fixed in versions: 2.0.18, 2.1.15, 2.2.6.
Workarounds
Until you can upgrade, apply the following workaround. It enforces ownership on the existing endpoints, so that:
• an authenticated shop user may only access payment requests of their own orders;
• an anonymous caller may only access payment requests of guest orders (the order's customer has no associated user account);
• everyone else receives `404 Not Found`.
Step 1. Add a query extension that filters the `GET` operation
Create file `src/ApiPlatform/QueryExtension/PaymentRequestOwnershipExtension.php`:
```php
sectionProvider->getSection() instanceof ShopApiSection) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$paymentJoin = $queryNameGenerator->generateJoinAlias('payment');
$orderJoin = $queryNameGenerator->generateJoinAlias('order');
$customerJoin = $queryNameGenerator->generateJoinAlias('customer');
$userJoin = $queryNameGenerator->generateJoinAlias('user');
$createdByGuestParameterName = $queryNameGenerator->generateParameterName('createdByGuest');
$queryBuilder
->innerJoin(sprintf('%s.payment', $rootAlias), $paymentJoin)
->innerJoin(sprintf('%s.order', $paymentJoin), $orderJoin)
->leftJoin(sprintf('%s.customer', $orderJoin), $customerJoin)
->leftJoin(sprintf('%s.user', $customerJoin), $userJoin)
;
$user = $this->userContext->getUser();
if ($user instanceof ShopUserInterface) {
$customerParam = $queryNameGenerator->generateParameterName('customer');
$queryBuilder
->andWhere($queryBuilder->expr()->eq(sprintf('%s.customer', $orderJoin), sprintf(':%s', $customerParam)))
->setParameter($customerParam, $user->getCustomer())
;
return;
}
$queryBuilder
->andWhere(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->isNull($userJoin),
$queryBuilder->expr()->isNull(sprintf('%s.customer', $orderJoin)),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->isNotNull($userJoin),
$queryBuilder->expr()->eq(sprintf('%s.createdByGuest', $orderJoin), sprintf(':%s', $createdByGuestParameterName)),
),
),
)
->setParameter($createdByGuestParameterName, true)
;
}
}
```
Step 2. Decorate the `PUT` state provider
Create file `src/ApiPlatform/StateProvider/PaymentRequestOwnershipProvider.php`:
```php
*/
final readonly class PaymentRequestOwnershipProvider implements ProviderInterface
{
/** @param ProviderInterface $inner */
public function __construct(
private ProviderInterface $inner,
private UserContextInterface $userContext,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object|null
{
$paymentRequest = $this->inner->provide($operation, $uriVariables, $context);
if (!$paymentRequest instanceof PaymentRequestInterface) {
return $paymentRequest;
}
if (!$this->isAccessible($paymentRequest)) {
return null;
}
return $paymentRequest;
}
private function isAccessible(PaymentRequestInterface $paymentRequest): bool
{
$payment = $paymentRequest->getPayment();
if (!$payment instanceof PaymentInterface) {
return false;
}
$order = $payment->getOrder();
if (!$order instanceof OrderInterface) {
return false;
}
$user = $this->userContext->getUser();
if ($user instanceof ShopUserInterface) {
$customer = $user->getCustomer();
return $customer instanceof CustomerInterface && $order->getCustomer() === $customer;
}
$customer = $order->getCustomer();
return null === $customer
|| null === $customer->getUser()
|| $order->isCreatedByGuest();
}
}
```
Step 3. Guard the `POST` creation endpoint with a command-bus middleware
The `POST /api/v2/shop/orders/{tokenValue}/payment-requests` operation is a `messenger: input` operation: it dispatches a `Sylius\Bundle\ApiBundle\Command\Payment\AddPaymentRequest` command whose `orderTokenValue` comes straight from the URL, so no query extension or state provider runs. Add a middleware on the Sylius command bus that loads the order, applies the same ownership rule, and aborts with `404` before the handler runs.
Create file `src/Messenger/Middleware/PaymentRequestOwnershipMiddleware.php`:
```php
<?php
declare(strict_types=1);
namespace App\Messenger\Middleware;
use Sylius\Bundle\ApiBundle\Command\Payment\AddPaymentRequest;
use Sylius\Bundle\ApiBundle\Context\UserContextInterface;
use Sylius\Component\Core\Mode…
Related
Alias · CVE (1)
- CVE-2026-53639 Sylius is an Open Source eCommerce Framework on Symfony. vulnerability
Weakness · CWE (1)
- CWE-639 Authorization Bypass Through User-Controlled Key weakness