GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-23RH-XW42-FQ82

Vulnerability from github – Published: 2026-09-10 19:25 – Updated: 2026-09-10 19:25
VLAI
Summary
Pimcore: SQL Injection in Custom Reports via Malicious Report Configuration
Details

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

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
  1. Access the report data endpoint to retrieve extracted user credentials
  2. 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:

// 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

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2026.1.5"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "pimcore/pimcore"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2026.1.0"
            },
            {
              "fixed": "2026.1.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 12.3.9"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "pimcore/pimcore"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-RC1"
            },
            {
              "fixed": "12.3.10"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c 11.5.18"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "pimcore/pimcore"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "11.5.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55416"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-10T19:25:05Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "# Security Advisory: SQL Injection in Custom Reports via Malicious Report Configuration\n\n## Summary\n\n### Impact\n\nA 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 \u2014 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.\n\nAdditionally, the LIMIT clause at line 51 directly interpolates `$offset` and `$limit` without integer casting, creating a secondary injection point.\n\n### Patches\n\nVersions 2026.1.6, 12.3.10, 11.5.19.\n\n### Workarounds\n\n1. Restrict `reports_config` permission to only highly trusted administrators\n2. Deploy a WAF rule to block requests to `/admin/bundle/customreports/custom-report/update` containing SQL keywords in the `configuration` parameter\n3. Replace the custom SQL adapter with a parameterized query builder approach\n\n## Attack Path (Validation Evidence)\n\n```\n[Entry Point] POST /admin/bundle/customreports/custom-report/update HTTP/1.1\n    \u2193 (requires reports_config permission + valid admin session)\n[Controller] CustomReportController::updateAction()\n    \u2193  $configuration = decodeJson($request-\u003erequest-\u003egetString(\u0027configuration\u0027))\n[Config Store] Configuration saved to custom_reports database table\n[Config Load] Tool\\Config::getByName() loads stdClass $config from DB\n    \u2193\n[Adapter] Sql::getBaseQuery() \u2192 Sql::buildQueryString($config)\n    \u2193  Directly concatenates config fields:\n[Vulnerable] $sql .= \"\\n\" . $config[\u0027sql\u0027];        // Line 92\n            $sql .= \"\\n\" . $config[\u0027from\u0027];        // Line 103\n            $sql .= \"\\n\" . \u0027WHERE (\u0027 . $config[\u0027where\u0027] . \u0027)\u0027; // Line 110\n            $sql .= \"\\n\" . $config[\u0027groupby\u0027];     // Line 117\n[Weak Guard] preg_match(\u0027/(ALTER|CREATE|DROP|RENAME|TRUNCATE|UPDATE|DELETE)\\s/i\u0027, ...)\n    \u2193  \u2717 Bypassable \u2014 missing INSERT, UNION, SELECT, subqueries, comments\n[Execution] $db-\u003efetchAllAssociative($sql);        // Line 54\n    \u2193\n[Impact] Arbitrary SQL execution \u2014 full database compromise\n```\n\n## Taint Flow (Validation Evidence)\n\n```\nSource: $request-\u003erequest-\u003egetString(\u0027configuration\u0027)  (HTTP POST body, user-controlled)\n    \u2193  json_decode() \u2192 stdClass\n[Store]  Persistent in database (custom_reports table)\n[Load]   Config::getByName() \u2192 stdClass $config\n    \u2193  \u2717 No sanitization (only bypassable regex blacklist)\n[Sink]   $db-\u003efetchAllAssociative($concatenatedSql)\n    \u2193\nImpact: Attacker-controlled SQL executed against the database\n```\n\n## Proof of Concept\n\n### Steps\n\n1. Authenticate as an admin user with `reports_config` permission\n2. Send a report update request with malicious SQL in the configuration:\n\n### Request\n\n```http\nPOST /admin/bundle/customreports/custom-report/update HTTP/1.1\nHost: \u003ctarget-host\u003e\nContent-Type: application/x-www-form-urlencoded\nCookie: PHPSESSID=\u003cvalid_admin_session\u003e\n\nname=malicious_report\u0026configuration=%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\n```\n\n3. Access the report data endpoint to retrieve extracted user credentials\n4. Alternatively, the `where` field can be set to:\n   ```\n   1=1 UNION SELECT TABLE_NAME, TABLE_SCHEMA, 1 FROM INFORMATION_SCHEMA.TABLES\n   ```\n   to enumerate all database tables\n\n### Expected Result\n\nThe custom report returns rows from arbitrary tables beyond what was intended, proving successful SQL injection.\n\n## Affected Component\n\n- **File:** `bundles/CustomReportsBundle/src/Tool/Adapter/Sql.php`\n- **Method:** `buildQueryString()` (lines 84-135), `getBaseQuery()` (lines 137-216), `getData()` (lines 25-58)\n- **Class:** `Pimcore\\Bundle\\CustomReportsBundle\\Tool\\Adapter\\Sql`\n\n## Fix Recommendation\n\nReplace the custom SQL concatenation approach with a parameterized query builder:\n\n```php\n// Instead of:\n$sql .= \"\\n\" . $config[\u0027sql\u0027];\n$sql .= \"\\n\" . $config[\u0027from\u0027];\n$sql .= \"\\n\" . \u0027WHERE (\u0027 . $config[\u0027where\u0027] . \u0027)\u0027;\n\n// Use a whitelist-based approach:\n// 1. Only allow predefined table names from a whitelist\n// 2. Use Doctrine QueryBuilder for WHERE conditions\n// 3. Use parameterized queries for all user-supplied values\n// 4. Cast LIMIT/OFFSET to integers\n\n$sql .= \u0027 LIMIT \u0027 . (int)$offset . \u0027,\u0027 . (int)$limit;\n```\n\n## Resources\n\n- [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)\n- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)",
  "id": "GHSA-23rh-xw42-fq82",
  "modified": "2026-09-10T19:25:05Z",
  "published": "2026-09-10T19:25:05Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pimcore/pimcore/security/advisories/GHSA-23rh-xw42-fq82"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pimcore/pimcore"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pimcore/pimcore/releases/tag/v11.5.19"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pimcore/pimcore/releases/tag/v12.3.10"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pimcore/pimcore/releases/tag/v2026.1.6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Pimcore: SQL Injection in Custom Reports via Malicious Report Configuration"
}



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…

Detection rules are retrieved from Rulezet.

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…