GitHub Security Advisory: GHSA-xcwx-r2gw-w93m
Package: sylius/sylius (COMPOSER)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), CWE-943: Improper Neutralization of Special Elements in Data Query Logic
Vulnerable versions: >= 2.2.0, <= 2.2.2
Patched in: 2.2.3

Summary: Sylius has a DQL Injection via API Order Filters

Details:
### Impact
Sylius API filters `ProductPriceOrderFilter` and `TranslationOrderNameAndLocaleFilter` pass user-supplied order direction values directly to Doctrine's `orderBy()` without validation. An attacker can inject arbitrary DQL:

```
GET /api/v2/shop/products?order[price]=ASC,%20variant.code%20DESC
```

### Patches
The issue is fixed in versions: 1.9.12, 1.10.16, 1.11.17, 1.12.23, 1.13.15, 1.14.18, 2.0.16, 2.1.12, 2.2.3 and above.

### Workarounds

An `EventSubscriber` that sanitizes `order` query parameters **only on API routes** before they reach the vulnerable filters.

The subscriber accepts an `$apiRoute` constructor parameter (default `/api/v2`) and skips non-API requests entirely — so there is zero overhead on shop/admin page requests.

This follows the same pattern used by Sylius's own `KernelRequestEventSubscriber` (`src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php`), which also uses `str_contains($pathInfo, $this->apiRoute)` to scope logic to API routes.

---

#### Step 1 — Create the EventSubscriber

`src/EventSubscriber/SanitizeOrderDirectionSubscriber.php`:

```php
<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class SanitizeOrderDirectionSubscriber implements EventSubscriberInterface
{
    private const ALLOWED_DIRECTIONS = ['asc', 'desc'];

    public function __construct(
        private string $apiRoute,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['sanitizeOrderParameters', 64],
        ];
    }

    public function sanitizeOrderParameters(RequestEvent $event): void
    {
        if (!str_contains($event->getRequest()->getPathInfo(), $this->apiRoute)) {
            return;
        }

        $request = $event->getRequest();

        /** @var mixed $order */
        $order = $request->query->all()['order'] ?? null;
        if (!is_array($order)) {
            return;
        }

        $needsSanitization = false;
        $sanitized = [];
        foreach ($order as $field => $direction) {
            if (is_string($direction) && in_array(strtolower($direction), self::ALLOWED_DIRECTIONS, true)) {
                $sanitized[$field] = $direction;
            } else {
                $needsSanitization = true;
            }
        }

        if (!$needsSanitization) {
            return;
        }

        $all = $request->query->all();
        $all['order'] = $sanitized;
        $request->query->replace($all);

        $request->server->set('QUERY_STRING', http_build_query($all));
        $request->attributes->set('_api_filters', $all);
    }
}
```

#### Step 2 — Register the service

**Option A** — If your `config/services.yaml` already has `App\` autowiring (Symfony default):

```yaml
# Nothing to do — autoconfigure picks up Eve