Blind SQL-injection in DAL aggregations in Shopware
Description
Shopware is an open headless commerce platform. The Shopware application API contains a search functionality which enables users to search through information stored within their Shopware instance. The searches performed by this function can be aggregated using the parameters in the “aggregations” object. The ‘name’ field in this “aggregations” object is vulnerable SQL-injection and can be exploited using time-based SQL-queries. This issue has been addressed and users are advised to update to Shopware 6.5.7.4. For older versions of 6.1, 6.2, 6.3 and 6.4 corresponding security measures are also available via a plugin. For the full range of functions, we recommend updating to the latest Shopware version.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
shopware/corePackagist | < 6.5.7.4 | 6.5.7.4 |
shopware/platformPackagist | < 6.5.7.4 | 6.5.7.4 |
Affected products
1Patches
25005213e609fNEXT-32887 - Improve SQL escaping in DAL
5 files changed · +39 −33
phpstan-baseline.neon+0 −10 modified@@ -4920,16 +4920,6 @@ parameters: count: 1 path: src/Core/Framework/DataAbstractionLayer/Command/CreateHydratorCommand.php - - - message: "#^Anonymous variable in a `\\$field\\-\\>\\.\\.\\.\\(\\)` method call can lead to false dead methods\\. Make sure the variable type is known$#" - count: 1 - path: src/Core/Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/JsonFieldAccessorBuilder.php - - - - message: "#^Method Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Dbal\\\\FieldAccessorBuilder\\\\JsonFieldAccessorBuilder\\:\\:getField\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Core/Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/JsonFieldAccessorBuilder.php - - message: "#^Method Shopware\\\\Core\\\\Framework\\\\DataAbstractionLayer\\\\Dbal\\\\QueryBuilder\\:\\:getStates\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1
src/Core/Framework/DataAbstractionLayer/Dbal/EntityAggregator.php+15 −15 modified@@ -293,11 +293,11 @@ private function parseDateHistogramAggregation( $query->addGroupBy($groupBy); $key = $aggregation->getName() . '.key'; - $query->addSelect(sprintf('MIN(%s) as `%s`', $accessor, $key)); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($key))); $key = $aggregation->getName() . '.count'; $countAccessor = $this->queryHelper->getFieldAccessor('id', $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(%s) as `%s`', $countAccessor, $key)); + $query->addSelect(sprintf('COUNT(%s) as %s', $countAccessor, EntityDefinitionQueryHelper::escape($key))); if ($aggregation->getSorting()) { $this->addSorting($aggregation->getSorting(), $definition, $query, $context); @@ -326,12 +326,12 @@ private function parseTermsAggregation( $keyAccessor = 'LOWER(HEX(' . $keyAccessor . '))'; } - $query->addSelect(sprintf('%s as `%s`', $keyAccessor, $key)); + $query->addSelect(sprintf('%s as %s', $keyAccessor, EntityDefinitionQueryHelper::escape($key))); $key = $aggregation->getName() . '.count'; $countAccessor = $this->queryHelper->getFieldAccessor('id', $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(%s) as `%s`', $countAccessor, $key)); + $query->addSelect(sprintf('COUNT(%s) as %s', $countAccessor, EntityDefinitionQueryHelper::escape($key))); if ($aggregation->getLimit()) { $query->setMaxResults($aggregation->getLimit()); @@ -354,7 +354,7 @@ private function parseAvgAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('AVG(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('AVG(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseSumAggregation( @@ -365,7 +365,7 @@ private function parseSumAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('SUM(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('SUM(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseMaxAggregation( @@ -376,7 +376,7 @@ private function parseMaxAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('MAX(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MAX(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseMinAggregation( @@ -387,7 +387,7 @@ private function parseMinAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('MIN(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseCountAggregation( @@ -398,7 +398,7 @@ private function parseCountAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(DISTINCT %s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('COUNT(DISTINCT %s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseStatsAggregation( @@ -410,16 +410,16 @@ private function parseStatsAggregation( $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); if ($aggregation->fetchAvg()) { - $query->addSelect(sprintf('AVG(%s) as `%s.avg`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('AVG(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.avg'))); } if ($aggregation->fetchMin()) { - $query->addSelect(sprintf('MIN(%s) as `%s.min`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.min'))); } if ($aggregation->fetchMax()) { - $query->addSelect(sprintf('MAX(%s) as `%s.max`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MAX(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.max'))); } if ($aggregation->fetchSum()) { - $query->addSelect(sprintf('SUM(%s) as `%s.sum`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('SUM(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.sum'))); } } @@ -445,7 +445,7 @@ private function parseRangeAggregation( $sum .= sprintf(' AND %s < %f', $accessor, $range['to']); } - $query->addSelect(sprintf('SUM(%s) as `%s.%s`', $sum, $aggregation->getName(), $id)); + $query->addSelect(sprintf('SUM(%s) as %s', $sum, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.' . $id))); } } @@ -459,7 +459,7 @@ private function parseEntityAggregation( $query->addGroupBy($accessor); $accessor = 'LOWER(HEX(' . $accessor . '))'; - $query->addSelect(sprintf('%s as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('%s as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } /**
src/Core/Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/JsonFieldAccessorBuilder.php+6 −3 modified@@ -58,9 +58,9 @@ public function buildAccessor(string $root, Field $field, Context $context, stri } $jsonValueExpr = sprintf( - 'JSON_EXTRACT(`%s`.`%s`, %s)', - $root, - $field->getStorageName(), + 'JSON_EXTRACT(%s.%s, %s)', + EntityDefinitionQueryHelper::escape($root), + EntityDefinitionQueryHelper::escape($field->getStorageName()), (string) $this->connection->quote('$' . $jsonPath) ); @@ -76,6 +76,9 @@ public function buildAccessor(string $root, Field $field, Context $context, stri return sprintf('IF(JSON_TYPE(%s) != "NULL", %s, NULL)', $jsonValueExpr, $accessor); } + /** + * @param Field[] $fields + */ private function getField(string $path, array $fields): ?Field { /** @var string $fieldName */
src/Core/Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/PriceFieldAccessorBuilder.php+6 −5 modified@@ -6,6 +6,7 @@ use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice; use Shopware\Core\Defaults; use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper; use Shopware\Core\Framework\DataAbstractionLayer\Field\Field; use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField; use Shopware\Core\Framework\Log\Package; @@ -74,11 +75,11 @@ public function buildAccessor(string $root, Field $field, Context $context, stri * We can indirectly cast to float by adding 0.0 */ - $template = '(JSON_UNQUOTE(JSON_EXTRACT(`#root#`.`#field#`, "$.c#currencyId#.#property#")) #factor#)'; + $template = '(JSON_UNQUOTE(JSON_EXTRACT(#root#.#field#, "$.c#currencyId#.#property#")) #factor#)'; $variables = [ - '#root#' => $root, - '#field#' => $field->getStorageName(), + '#root#' => EntityDefinitionQueryHelper::escape($root), + '#field#' => EntityDefinitionQueryHelper::escape($field->getStorageName()), '#currencyId#' => $currencyId, '#property#' => $jsonAccessor, '#factor#' => '+ 0.0', @@ -88,8 +89,8 @@ public function buildAccessor(string $root, Field $field, Context $context, stri if ($currencyId !== Defaults::CURRENCY) { $variables = [ - '#root#' => $root, - '#field#' => $field->getStorageName(), + '#root#' => EntityDefinitionQueryHelper::escape($root), + '#field#' => EntityDefinitionQueryHelper::escape($field->getStorageName()), '#currencyId#' => Defaults::CURRENCY, '#property#' => $jsonAccessor, '#factor#' => $currencyFactor,
src/Core/Framework/Test/DataAbstractionLayer/Search/EntityAggregatorTest.php+12 −0 modified@@ -1328,6 +1328,18 @@ public function testAggregateNonExistingShouldFail(): void $this->aggregator->aggregate($this->getContainer()->get(TaxDefinition::class), $criteria, $context); } + public function testAggregationWithBacktickInName(): void + { + $context = Context::createDefaultContext(); + + $criteria = new Criteria(); + $criteria->addAggregation(new SumAggregation('`taxRate`', 'taxRate')); + + static::expectException(\InvalidArgumentException::class); + static::expectExceptionMessage('Backtick not allowed in identifier'); + $this->aggregator->aggregate($this->getContainer()->get(TaxDefinition::class), $criteria, $context); + } + private function insertData(): void { $repository = $this->getContainer()->get('product.repository');
e2256ec81e56NEXT-32887 - Improve SQL escaping in DAL
4 files changed · +39 −23
Framework/DataAbstractionLayer/Dbal/EntityAggregator.php+15 −15 modified@@ -293,11 +293,11 @@ private function parseDateHistogramAggregation( $query->addGroupBy($groupBy); $key = $aggregation->getName() . '.key'; - $query->addSelect(sprintf('MIN(%s) as `%s`', $accessor, $key)); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($key))); $key = $aggregation->getName() . '.count'; $countAccessor = $this->queryHelper->getFieldAccessor('id', $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(%s) as `%s`', $countAccessor, $key)); + $query->addSelect(sprintf('COUNT(%s) as %s', $countAccessor, EntityDefinitionQueryHelper::escape($key))); if ($aggregation->getSorting()) { $this->addSorting($aggregation->getSorting(), $definition, $query, $context); @@ -326,12 +326,12 @@ private function parseTermsAggregation( $keyAccessor = 'LOWER(HEX(' . $keyAccessor . '))'; } - $query->addSelect(sprintf('%s as `%s`', $keyAccessor, $key)); + $query->addSelect(sprintf('%s as %s', $keyAccessor, EntityDefinitionQueryHelper::escape($key))); $key = $aggregation->getName() . '.count'; $countAccessor = $this->queryHelper->getFieldAccessor('id', $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(%s) as `%s`', $countAccessor, $key)); + $query->addSelect(sprintf('COUNT(%s) as %s', $countAccessor, EntityDefinitionQueryHelper::escape($key))); if ($aggregation->getLimit()) { $query->setMaxResults($aggregation->getLimit()); @@ -354,7 +354,7 @@ private function parseAvgAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('AVG(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('AVG(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseSumAggregation( @@ -365,7 +365,7 @@ private function parseSumAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('SUM(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('SUM(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseMaxAggregation( @@ -376,7 +376,7 @@ private function parseMaxAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('MAX(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MAX(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseMinAggregation( @@ -387,7 +387,7 @@ private function parseMinAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('MIN(%s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseCountAggregation( @@ -398,7 +398,7 @@ private function parseCountAggregation( ): void { $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); - $query->addSelect(sprintf('COUNT(DISTINCT %s) as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('COUNT(DISTINCT %s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } private function parseStatsAggregation( @@ -410,16 +410,16 @@ private function parseStatsAggregation( $accessor = $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition, $definition->getEntityName(), $context); if ($aggregation->fetchAvg()) { - $query->addSelect(sprintf('AVG(%s) as `%s.avg`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('AVG(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.avg'))); } if ($aggregation->fetchMin()) { - $query->addSelect(sprintf('MIN(%s) as `%s.min`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MIN(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.min'))); } if ($aggregation->fetchMax()) { - $query->addSelect(sprintf('MAX(%s) as `%s.max`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('MAX(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.max'))); } if ($aggregation->fetchSum()) { - $query->addSelect(sprintf('SUM(%s) as `%s.sum`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('SUM(%s) as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.sum'))); } } @@ -445,7 +445,7 @@ private function parseRangeAggregation( $sum .= sprintf(' AND %s < %f', $accessor, $range['to']); } - $query->addSelect(sprintf('SUM(%s) as `%s.%s`', $sum, $aggregation->getName(), $id)); + $query->addSelect(sprintf('SUM(%s) as %s', $sum, EntityDefinitionQueryHelper::escape($aggregation->getName() . '.' . $id))); } } @@ -459,7 +459,7 @@ private function parseEntityAggregation( $query->addGroupBy($accessor); $accessor = 'LOWER(HEX(' . $accessor . '))'; - $query->addSelect(sprintf('%s as `%s`', $accessor, $aggregation->getName())); + $query->addSelect(sprintf('%s as %s', $accessor, EntityDefinitionQueryHelper::escape($aggregation->getName()))); } /**
Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/JsonFieldAccessorBuilder.php+6 −3 modified@@ -58,9 +58,9 @@ public function buildAccessor(string $root, Field $field, Context $context, stri } $jsonValueExpr = sprintf( - 'JSON_EXTRACT(`%s`.`%s`, %s)', - $root, - $field->getStorageName(), + 'JSON_EXTRACT(%s.%s, %s)', + EntityDefinitionQueryHelper::escape($root), + EntityDefinitionQueryHelper::escape($field->getStorageName()), (string) $this->connection->quote('$' . $jsonPath) ); @@ -76,6 +76,9 @@ public function buildAccessor(string $root, Field $field, Context $context, stri return sprintf('IF(JSON_TYPE(%s) != "NULL", %s, NULL)', $jsonValueExpr, $accessor); } + /** + * @param Field[] $fields + */ private function getField(string $path, array $fields): ?Field { /** @var string $fieldName */
Framework/DataAbstractionLayer/Dbal/FieldAccessorBuilder/PriceFieldAccessorBuilder.php+6 −5 modified@@ -6,6 +6,7 @@ use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice; use Shopware\Core\Defaults; use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper; use Shopware\Core\Framework\DataAbstractionLayer\Field\Field; use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField; use Shopware\Core\Framework\Log\Package; @@ -74,11 +75,11 @@ public function buildAccessor(string $root, Field $field, Context $context, stri * We can indirectly cast to float by adding 0.0 */ - $template = '(JSON_UNQUOTE(JSON_EXTRACT(`#root#`.`#field#`, "$.c#currencyId#.#property#")) #factor#)'; + $template = '(JSON_UNQUOTE(JSON_EXTRACT(#root#.#field#, "$.c#currencyId#.#property#")) #factor#)'; $variables = [ - '#root#' => $root, - '#field#' => $field->getStorageName(), + '#root#' => EntityDefinitionQueryHelper::escape($root), + '#field#' => EntityDefinitionQueryHelper::escape($field->getStorageName()), '#currencyId#' => $currencyId, '#property#' => $jsonAccessor, '#factor#' => '+ 0.0', @@ -88,8 +89,8 @@ public function buildAccessor(string $root, Field $field, Context $context, stri if ($currencyId !== Defaults::CURRENCY) { $variables = [ - '#root#' => $root, - '#field#' => $field->getStorageName(), + '#root#' => EntityDefinitionQueryHelper::escape($root), + '#field#' => EntityDefinitionQueryHelper::escape($field->getStorageName()), '#currencyId#' => Defaults::CURRENCY, '#property#' => $jsonAccessor, '#factor#' => $currencyFactor,
Framework/Test/DataAbstractionLayer/Search/EntityAggregatorTest.php+12 −0 modified@@ -1328,6 +1328,18 @@ public function testAggregateNonExistingShouldFail(): void $this->aggregator->aggregate($this->getContainer()->get(TaxDefinition::class), $criteria, $context); } + public function testAggregationWithBacktickInName(): void + { + $context = Context::createDefaultContext(); + + $criteria = new Criteria(); + $criteria->addAggregation(new SumAggregation('`taxRate`', 'taxRate')); + + static::expectException(\InvalidArgumentException::class); + static::expectExceptionMessage('Backtick not allowed in identifier'); + $this->aggregator->aggregate($this->getContainer()->get(TaxDefinition::class), $criteria, $context); + } + private function insertData(): void { $repository = $this->getContainer()->get('product.repository');
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-qmp9-2xwj-m6m9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-22406ghsaADVISORY
- github.com/shopware/core/commit/e2256ec81e56f792623e90d89786d8a9fcad28bfghsaWEB
- github.com/shopware/shopware/commit/5005213e609f5a4423fcfa92f105c3de8ab35100ghsaWEB
- github.com/shopware/shopware/releases/tag/v6.5.7.4ghsaWEB
- github.com/shopware/shopware/security/advisories/GHSA-qmp9-2xwj-m6m9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.