GitHub Advisoryadvisory · Packagist
GHSA-9x44-4gxf-8c25 Pimcore Vulnerable to Remote Code Execution via DataObject Class-Definition Field Name
CRITICAL
Official page ↗
- Aliases
- CVE-2026-55634
- Ecosystem
- Packagist
- Packages
- pimcore/pimcore
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-89, CWE-94
- Reviewed
- 2026-08-28
Affected packages
• Packagist pimcore/pimcore: < 12.3.10 → fixed in 12.3.10
• Packagist pimcore/pimcore: >= 2026.1.0, < 2026.1.6 → fixed in 2026.1.6
Description
Overview
A DataObject class-definition field name is concatenated, without an identifier allowlist, into the PHP class source that Pimcore generates for every DataObject class (`protected $;`). A user holding only the ordinary `objects` (DataObjects) permission can import a class definition whose field name closes the property and injects arbitrary PHP into the generated class file, achieving remote code execution on the server. The same unvalidated field name is also concatenated into ALTER TABLE DDL (`ADD COLUMN`/`ADD INDEX`), giving a parallel SQL-injection primitive. This is a sibling of CVE-2026-5394 (composite-index column SQL injection); that fix hardened only the `compositeIndices` sink and left the field-name path untouched.
Impact
Any authenticated user with the `objects` permission — the standard permission for content editors who work with DataObjects, not an administrator or a dedicated "classes" permission — can:
1. Execute arbitrary PHP on the server (RCE). The injected code runs in the web application's PHP process when an object of the affected class is loaded (and is re-executed on every load), with full access to the application, its database credentials, secrets, and the host filesystem/OS — i.e. full server compromise.
2. Execute arbitrary ALTER TABLE DDL (SQL injection) against the DataObject store/query tables (drop columns, add indexes, corrupt schema).
Confidence (read with the Reproduction section). The RCE sink — the real builder emitting attacker PHP into the generated class body, that class loading, and its `__construct()` executing a shell command — is runtime-confirmed in an isolated harness (see Reproduction → "Lab confirmation"). The remaining links of the end-to-end chain are reasoned from source but not yet run end-to-end on a live Pimcore: (a) the Studio import path (`generateLayoutTreeFromArray` → `save`) preserving the field name without transform/reject; (b) the persistent-field DDL step not aborting the save (addressed by the ≤64-byte gadget); and (c) Pimcore instantiating the object (`new`, e.g. via `DataObject::getById()`) so `__construct()` fires — autoloading alone executes only top-level class-body code, not the constructor. Treat the RCE as sink-confirmed + chain-reasoned, not as a fully-executed live exploit.
Because the injected PHP executes with the privileges of the PHP runtime (typically the web-server user) and reaches the operating system — beyond the authority of the Pimcore application account the attacker started from — the scope is assessed Changed (`S:C`), consistent with Pimcore's own scoring of the analogous Custom-Reports SQL injection (GHSA-3234-gxc3-pq6f, `AV:N/AC:L/PR:L/UI:R/S:C`, 8.7); the result here is RCE rather than read-only SQLi, yielding 9.9 Critical. `S:C` is the one debatable metric: a reviewer who scores the impact within the single PHP/OS authority as `S:U` lands at `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` = 8.8 High. The severity floor is therefore High regardless of the scope interpretation.
Technical Details
Source → sink (RCE)
Pimcore generates a PHP class file for every DataObject class. The property block is built in `lib/DataObject/ClassBuilder/FieldDefinitionPropertiesBuilder.php`:
```php // lib/DataObject/ClassBuilder/FieldDefinitionPropertiesBuilder.php:27-32 foreach ($classDefinition->getFieldDefinitions() as $key => $def) { if (!$def instanceof ClassDefinition\Data\ReverseObjectRelation && !$def instanceof ClassDefinition\Data\CalculatedValue) { $cd .= 'protected $'.$key.";\n"; // $key = field NAME, concatenated raw into PHP source } } ```
`$key` is the field name. The string is assembled into a class body in `lib/DataObject/ClassBuilder/ClassBuilder.php:104-112` (`class extends {\n` + properties), written to `var/classes/DataObject/.php`, and autoloaded/included. A field name such as:
``` poc; public function __construct(){ /* attacker PHP */ } private $z ```
produces a valid class body containing an attacker-defined `__construct()` that executes when an object of the class is loaded.
That the maintainers know name→PHP-generation requires an identifier allowlist is shown by the sibling enum-option generator, which does enforce one:
```php // lib/DataObject/ClassBuilder/SelectOptionsEnumBuilder.php:188 if (!preg_match('/^[A-Z-a-z_][A-Za-z0-9_]*$/', $selectOptionName)) { /* reject */ } ```
The field-name path has no equivalent.
Parallel SQL-injection sink
The same field name is concatenated, with backtick string quoting (not `quoteIdentifier`), into DDL:
```php // models/DataObject/ClassDefinition/Helper/Dao.php:102 (addModifyColumn — ADD COLUMN) $this->db->executeQuery('ALTER TABLE ' . $table . ' ADD COLUMN ' . $colName . ' ' . $type . ...); // :52/:67 (addIndexToField — ADD INDEX ()) $this->db->executeQuery('ALTER TABLE ' . $table . ' ADD ' . $uniqueStr . 'INDEX ' . $prefix . $indexName . ' (' . $columnName . ');'); ```
Source: `models/DataObject/ClassDefinition/Dao.php:228` → `$this->addModifyColumn($objectDatastoreTable, $key, $value->getColumnType(), '', 'NULL')`, `$key` = field name. A backtick in the field name breaks out of the quoted identifier.
Contrast the patched composite-index sink, now guarded by an allowlist and `quoteIdentifier` (`models/DataObject/Traits/CompositeIndexTrait.php`).
Why validation does not stop it
The complete field-name validation across the import → save path:
1. `models/DataObject/ClassDefinition/Service.php:296` (`generateLayoutTreeFromArray`): `preg_match('//', $name)` — rejects only angle-bracket names. Backtick, `;`, `{}`, `()`, quotes, spaces all pass.
2. 2. `models/DataObject/ClassDefinition/Data.php:1292` (`isForbiddenName()`) — a reserved-word denylist (`in_array(strtolower($name), FORBIDDEN_NAMES)`), no character filtering.
3. 3. `models/DataObject/ClassDefinition.php:1149` — validates the class name/id only.
No allowlist is applied to field names. The Studio UI enforces an identifier pattern client-side; the API does not.
Reachability / privilege
The HTTP entry point (`pimcore/studio-backend-bundle`):
```php // src/Class/Controller/DefinitionConfiguration/ImportController.php private const string ROUTE = '/class/definition/configuration-view/detail/{id}/import';
[Route(self::ROUTE, name: 'pimcore_studio_api_class_definition_import', methods: ['POST'])] #[IsGranted(UserPermissions::DATA_OBJECTS->value)] // UserPermissions::DATA_OBJECTS = 'objects' public function importClassDefinition(string $id, #[MapUploadedFile] UploadedFile $file): JsonResponse { return $this->jsonResponse( $this->classDefinitionService->importClassDefinitionFromJson($id, $file->getContent()) ); } ```
`importClassDefinitionFromJson` → `ClassDefinitionRepository::importFromJson` → model `save()` → `saveClassInternal()`, which runs the DDL (`getDao()->save()`) and then the PHP class generation (`generateClassFilesInternal()`). The endpoint requires only the `objects` permission (`PR:L`) and performs no field-name validation of its own. The single authorization gate is the route-level `#[IsGranted('objects')]`; a DataObject class definition is global schema (not a workspace-scoped element), so no element-/workspace-level secondary authorization applies to the import — `objects` alone reaches the sink, which is what anchors `PR:L`. This is the same import endpoint used in the CVE-2026-5394 PoC.
Execution order
In `saveClassInternal()`: field denylist check → class-name regex → `getDao()->save()` (DDL sink fires) → `generateClassFilesInternal()` (PHP-gen sink fires). The SQLi triggers first; the RCE payload either uses a non-persistent field type (no `ADD COLUMN`) or a ≤64-byte DDL-valid name so the DDL step does not abort before PHP generation.
Reproduction
Lab confirmation of the RCE sink (runtime, verified)
Using the unmodified `FieldDefinitionPropertiesBuilder` source driven by a minimal `Cla…
Related
Alias · CVE (1)
- CVE-2026-55634 Pimcore is an Open Source Data & Experience Management Platform. vulnerability
References
- https://github.com/pimcore/pimcore/security/advisories/GHSA-9x44-4gxf-8c25 web
- https://github.com/pimcore/pimcore/pull/19183 web
- https://github.com/pimcore/pimcore/commit/a4f8c3cfee58b7d5fe4873d67782eff58dae9b9d web
- https://github.com/pimcore/pimcore package
- https://github.com/pimcore/pimcore/releases/tag/v2026.1.6 web