{"id":"CVE-2026-31825","aliases":["GHSA-xcwx-r2gw-w93m"],"url":"https://o3.security/vulnerability/CVE-2026-31825","summary":"Sylius has a DQL Injection via API Order Filters","details":"### Impact\nSylius API filters `ProductPriceOrderFilter` and `TranslationOrderNameAndLocaleFilter` pass user-supplied order direction values directly to Doctrine's `orderBy()` without validation. An attacker can inject arbitrary DQL:\n\n```\nGET /api/v2/shop/products?order[price]=ASC,%20variant.code%20DESC\n```\n\n### Patches\nThe 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.\n\n### Workarounds\n\nAn `EventSubscriber` that sanitizes `order` query parameters **only on API routes** before they reach the vulnerable filters.\n\nThe 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.\n\nThis 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.\n\n---\n\n#### Step 1 — Create the EventSubscriber\n\n`src/EventSubscriber/SanitizeOrderDirectionSubscriber.php`:\n\n```php\n<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\EventSubscriber;\n\nuse Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\nuse Symfony\\Component\\HttpKernel\\KernelEvents;\n\nfinal class SanitizeOrderDirectionSubscriber implements EventSubscriberInterface\n{\n    private const ALLOWED_DIRECTIONS = ['asc', 'desc'];\n\n    public function __construct(\n        private string $apiRoute,\n    ) {\n    }\n\n    public static function getSubscribedEvents(): array\n    {\n        return [\n            KernelEvents::REQUEST => ['sanitizeOrderParameters', 64],\n        ];\n    }\n\n    public function sanitizeOrderParameters(RequestEvent $event): void\n    {\n        if (!str_contains($event->getRequest()->getPathInfo(), $this->apiRoute)) {\n            return;\n        }\n\n        $request = $event->getRequest();\n\n        /** @var mixed $order */\n        $order = $request->query->all()['order'] ?? null;\n        if (!is_array($order)) {\n            return;\n        }\n\n        $needsSanitization = false;\n        $sanitized = [];\n        foreach ($order as $field => $direction) {\n            if (is_string($direction) && in_array(strtolower($direction), self::ALLOWED_DIRECTIONS, true)) {\n                $sanitized[$field] = $direction;\n            } else {\n                $needsSanitization = true;\n            }\n        }\n\n        if (!$needsSanitization) {\n            return;\n        }\n\n        $all = $request->query->all();\n        $all['order'] = $sanitized;\n        $request->query->replace($all);\n\n        $request->server->set('QUERY_STRING', http_build_query($all));\n        $request->attributes->set('_api_filters', $all);\n    }\n}\n```\n\n#### Step 2 — Register the service\n\n**Option A** — If your `config/services.yaml` already has `App\\` autowiring (Symfony default):\n\n```yaml\n# Nothing to do — autoconfigure picks up EventSubscriberInterface automatically.\n# Optionally bind the API route prefix:\nservices:\n    App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n        arguments:\n            $apiRoute: '%sylius.security.new_api_route%'\n```\n\n**Option B** — If there is no `App\\` autowiring:\n\n```yaml\nservices:\n    App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n        arguments:\n            $apiRoute: '%sylius.security.new_api_route%'\n        tags: ['kernel.event_subscriber']\n```\n\nUsing `%sylius.security.new_api_route%` ties the subscriber to the same prefix Sylius uses (`/api/v2` by default). If the parameter is not available, hardcode `'/api/v2'` instead.\n\n#### Step 3 — Clear cache\n\n```bash\nbin/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- Chris Alupului (@Neosprings)\n- Bartłomiej Nowiński (@bnBart)\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)","published":"2026-03-10T21:33:26.471Z","modified":"2026-08-12T03:51:41.176529317Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.9.12"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.10.16"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.11.17"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.12.23"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.13.15"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"1.14.18"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"2.0.16"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"2.1.12"},{"ecosystem":"Packagist","name":"sylius/sylius","fixedVersion":"2.2.3"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/31xxx/CVE-2026-31825.json"},{"type":"ADVISORY","url":"https://github.com/Sylius/Sylius/security/advisories/GHSA-xcwx-r2gw-w93m"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-31825"},{"type":"PACKAGE","url":"https://github.com/Sylius/Sylius"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:41.176529317Z"}}