GHSA-XCWX-R2GW-W93M

Vulnerability from github – Published: 2026-03-11 00:13 – Updated: 2026-03-11 20:33
VLAI
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

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:

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…