CVE-2026-55416: 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 Custom Reports bundle (Sql adapter)to a version that resolves this vulnerability.Fixed in 2026.1.6 - Upgrade
Upgrade
Pimcore Custom Reports bundle (Sql adapter)to a version that resolves this vulnerability.Fixed in 12.3.10 - Upgrade
Upgrade
Pimcore Custom Reports bundle (Sql adapter)to a version that resolves this vulnerability.Fixed in 11.5.19 - Configuration
Restrict the 'reports_config' permission to only highly trusted administrators, since an authenticated attacker with this permission can inject arbitrary SQL via report configuration fields ('sql', 'from', 'where', 'groupby') that are concatenated into SQL queries.
Pimcore Custom Reports (CustomReportController/updateAction → CustomReportsBundle Tool Adapter Sql) reports_config permission scope = restrict to highly trusted administrators - Configuration
Cast LIMIT/OFFSET to integers before interpolating into the SQL string (the advisory notes the LIMIT clause directly interpolates '$offset' and '$limit' without integer casting, creating a secondary injection point).
Pimcore Custom Reports (Sql::buildQueryString / getBaseQuery) LIMIT/OFFSET handling = cast to integers - Configuration
Replace the custom SQL concatenation approach with a parameterized query builder approach (Doctrine QueryBuilder), using parameterized queries for all user-supplied values (including WHERE conditions), instead of concatenating $config fields into SQL.
Pimcore Custom Reports (Sql adapter) SQL query construction = use parameterized queries / Doctrine QueryBuilder for WHERE conditions - Configuration
Only allow predefined table names from a whitelist for report configuration fields such as 'from', rather than using attacker-controlled table identifiers directly.
Pimcore Custom Reports (Sql adapter) table selection = whitelist-based approach - Compensating control
Deploy a WAF rule to block requests to /admin/bundle/customreports/custom-report/update containing SQL keywords in the 'configuration' parameter (HTTP POST body).
- Compensating control
Restrict access to the report update endpoint (/admin/bundle/customreports/custom-report/update) to trusted users/sessions with the required 'reports_config' permission; the advisory notes exploitation requires an authenticated admin session with that permission.
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 only highly trusted administrators reduces exposure.
Which report settings are dangerous?
The sql, from, where, and groupby report configuration fields are concatenated directly into database queries. The offset and limit values in the LIMIT clause are also directly interpolated without integer casting.
What can an attacker do after exploiting it?
The advisory states that exploitation can allow an attacker to read, modify, or delete all database data. It also identifies techniques such as UNION SELECT, INSERT, LOAD_FILE(), INTO OUTFILE, stacked queries, subqueries, and MySQL comment injection as not blocked by the keyword blacklist.
Which releases contain patches?
The advisory lists patched versions 2026.1.6, 12.3.10, and 11.5.19.