GHSA-XCWX-R2GW-W93M
Vulnerability from github – Published: 2026-03-11 00:13 – Updated: 2026-03-11 20:33Impact
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
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):
# Nothing to do — autoconfigure picks up EventSubscriberInterface automatically.
# Optionally bind the API route prefix:
services:
App\EventSubscriber\SanitizeOrderDirectionSubscriber:
arguments:
$apiRoute: '%sylius.security.new_api_route%'
Option B — If there is no App\ autowiring:
services:
App\EventSubscriber\SanitizeOrderDirectionSubscriber:
arguments:
$apiRoute: '%sylius.security.new_api_route%'
tags: ['kernel.event_subscriber']
Using %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.
Step 3 — Clear cache
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: - Chris Alupului (@Neosprings) - Bartłomiej Nowiński (@bnBart)
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
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.9.11"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.12"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.10.15"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "1.10.0"
},
{
"fixed": "1.10.16"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.11.16"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "1.11.0"
},
{
"fixed": "1.11.17"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.12.22"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "1.12.0"
},
{
"fixed": "1.12.23"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.13.14"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "1.13.0"
},
{
"fixed": "1.13.15"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.14.17"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "1.14.0"
},
{
"fixed": "1.14.18"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.0.15"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.0.16"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.1.11"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "2.1.0"
},
{
"fixed": "2.1.12"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.2.2"
},
"package": {
"ecosystem": "Packagist",
"name": "sylius/sylius"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-31825"
],
"database_specific": {
"cwe_ids": [
"CWE-89",
"CWE-943"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-11T00:13:41Z",
"nvd_published_at": "2026-03-10T22:16:20Z",
"severity": "MODERATE"
},
"details": "### Impact\nSylius API filters `ProductPriceOrderFilter` and `TranslationOrderNameAndLocaleFilter` pass user-supplied order direction values directly to Doctrine\u0027s `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 \u2014 so there is zero overhead on shop/admin page requests.\n\nThis follows the same pattern used by Sylius\u0027s own `KernelRequestEventSubscriber` (`src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php`), which also uses `str_contains($pathInfo, $this-\u003eapiRoute)` to scope logic to API routes.\n\n---\n\n#### Step 1 \u2014 Create the EventSubscriber\n\n`src/EventSubscriber/SanitizeOrderDirectionSubscriber.php`:\n\n```php\n\u003c?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 = [\u0027asc\u0027, \u0027desc\u0027];\n\n public function __construct(\n private string $apiRoute,\n ) {\n }\n\n public static function getSubscribedEvents(): array\n {\n return [\n KernelEvents::REQUEST =\u003e [\u0027sanitizeOrderParameters\u0027, 64],\n ];\n }\n\n public function sanitizeOrderParameters(RequestEvent $event): void\n {\n if (!str_contains($event-\u003egetRequest()-\u003egetPathInfo(), $this-\u003eapiRoute)) {\n return;\n }\n\n $request = $event-\u003egetRequest();\n\n /** @var mixed $order */\n $order = $request-\u003equery-\u003eall()[\u0027order\u0027] ?? null;\n if (!is_array($order)) {\n return;\n }\n\n $needsSanitization = false;\n $sanitized = [];\n foreach ($order as $field =\u003e $direction) {\n if (is_string($direction) \u0026\u0026 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-\u003equery-\u003eall();\n $all[\u0027order\u0027] = $sanitized;\n $request-\u003equery-\u003ereplace($all);\n\n $request-\u003eserver-\u003eset(\u0027QUERY_STRING\u0027, http_build_query($all));\n $request-\u003eattributes-\u003eset(\u0027_api_filters\u0027, $all);\n }\n}\n```\n\n#### Step 2 \u2014 Register the service\n\n**Option A** \u2014 If your `config/services.yaml` already has `App\\` autowiring (Symfony default):\n\n```yaml\n# Nothing to do \u2014 autoconfigure picks up EventSubscriberInterface automatically.\n# Optionally bind the API route prefix:\nservices:\n App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n arguments:\n $apiRoute: \u0027%sylius.security.new_api_route%\u0027\n```\n\n**Option B** \u2014 If there is no `App\\` autowiring:\n\n```yaml\nservices:\n App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n arguments:\n $apiRoute: \u0027%sylius.security.new_api_route%\u0027\n tags: [\u0027kernel.event_subscriber\u0027]\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 `\u0027/api/v2\u0027` instead.\n\n#### Step 3 \u2014 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\u0142omiej Nowi\u0144ski (@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)",
"id": "GHSA-xcwx-r2gw-w93m",
"modified": "2026-03-11T20:33:18Z",
"published": "2026-03-11T00:13:41Z",
"references": [
{
"type": "WEB",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Sylius has a DQL Injection via API Order Filters"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.