GHSA-23rh-xw42-fq82: SQL Injection
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 reportsconfig 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, LOADFILE(), 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 reportsconfig 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 reportsconfig permission + valid admin session) [Controller] CustomReportController::updateAction() ↓ $configuration = decodeJson($request->request->getString('configuration')) [Config Store] Configuration saved to customreports 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] pregmatch('/(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) ↓ jsondecode() → stdClass [Store] Persistent in database (customreports 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 reportsconfig 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=<validadminsession>
name=maliciousreport&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 TABLENAME, TABLESCHEMA, 1 FROM INFORMATIONSCHEMA.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 - OWASP SQL Injection Prevention Cheat Sheet
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/pimcore/pimcoreto a version that resolves this vulnerability.Fixed in 11.5.19 - Upgrade
Upgrade
composer/pimcore/pimcoreto a version that resolves this vulnerability.Fixed in 12.3.10 - Upgrade
Upgrade
composer/pimcore/pimcoreto a version that resolves this vulnerability.Fixed in 2026.1.6 - Upgrade
Upgrade
Pimcore CustomReportsBundle (pimcore/bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php)to a version that resolves this vulnerability.Fixed in 2026.1.6 - Upgrade
Upgrade
Pimcore CustomReportsBundle (pimcore/bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php)to a version that resolves this vulnerability.Fixed in 12.3.10 - Upgrade
Upgrade
Pimcore CustomReportsBundle (pimcore/bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php)to a version that resolves this vulnerability.Fixed in 11.5.19 - Configuration
Replace the custom SQL concatenation approach in Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\Sql::{buildQueryString(), getBaseQuery()} with a parameterized query builder approach (use Doctrine QueryBuilder for WHERE conditions and parameterized queries for all user-supplied values from the report configuration fields: sql/from/where/groupby).
CustomReportsBundle Sql adapter (Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\Sql) buildQueryString/getBaseQuery parameterization = parameterized queries (Doctrine QueryBuilder) - Configuration
Ensure LIMIT/OFFSET values used in Sql::getBaseQuery()/buildQueryString are cast to integers (secondary injection point at the LIMIT clause where $offset and $limit are interpolated).
CustomReportsBundle Sql adapter (Pimcore\Bundle\CustomReportsBundle\Tool\Adapter\Sql) LIMIT/OFFSET handling = cast (int) for both $limit and $offset
Event History
Frequently Asked Questions
Who can exploit this issue?
An attacker must be authenticated and have the reports_config permission. Restricting that permission to highly trusted administrators reduces the exposed user population.
What access could exploitation provide?
An attacker can inject SQL through report configuration fields and may read, modify, or delete all database data. The advisory describes this as complete data compromise.
Which releases include patches?
The listed patched versions are 2026.1.6, 12.3.10, and 11.5.19.
What should be done if patching cannot happen immediately?
Restrict reports_config permission to only highly trusted administrators. The advisory also recommends deploying a WAF rule, but the provided information does not specify the rule details.