Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"schema_version": "1.4.0",
"id": "GHSA-h4ph-crvj-9h92",
"modified": "2026-05-27T00:35:56Z",
"modified": "2026-05-27T00:36:25Z",
"published": "2026-05-27T00:35:56Z",
"aliases": [
"CVE-2026-44741"
],
"summary": "Pimcore Admin Classic Bundle Vulnerable to SQL Injection in Translation Grid Date Filter via Unsanitized Property Parameter",
"details": "# GitHub Security Advisory Draft — GM-369\n\n## Summary\nSQL injection in Pimcore's translation grid date filter — the user-supplied `property` field from the filter JSON is interpolated directly into a `UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(...)))` SQL expression without parameterization or allowlist validation.\n\n## Affected Component\n- **Package:** `pimcore/admin-ui-classic-bundle`\n- **File:** `src/Controller/Admin/TranslationController.php`\n- **Lines:** 565 (input), 569 (inadequate sanitization), 593 (injection point)\n- **Endpoint:** `POST /admin/translation/translations`\n\n## Description\nThe translation grid endpoint processes JSON filter parameters. When a filter has `type: \"date\"`, the `property` field is extracted and used to construct a SQL expression:\n\n```php\n$fieldname = $filter[$propertyField]; // Line 565 — user input\n$fieldname = str_replace('--', '', $fieldname); // Line 569 — trivially bypassable\n$fieldname = $tableName . '.' . $fieldname; // Line 577\n$fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\"; // Line 593 — injection\n```\n\nThe `str_replace('--', '')` sanitization is trivially bypassable (use `/**/` comments or `----`). In non-language mode, `$fieldname` is concatenated directly into the SQL condition without quoting or parameterization.\n\n## Impact\nAuthenticated user with translations view permission can extract arbitrary database data via UNION-based or error-based SQL injection. Combined with GM-249 (unsafe unserialize), this enables an SQLi → deserialization → RCE chain.\n\n## Proof of Concept\n```\nPOST /admin/translation/translations\nfilter=[{\"property\":\"1))) UNION SELECT password FROM users WHERE ((1\",\"type\":\"date\",\"operator\":\"eq\",\"value\":\"2026-01-01\"}]\n```\n\n## Suggested Fix\nValidate `$fieldname` against an allowlist of valid column names before SQL interpolation:\n```php\n$allowedDateColumns = ['creationDate', 'modificationDate'];\nif (!in_array($fieldname, $allowedDateColumns, true)) {\n continue;\n}\n```\n\n## References\n- CWE-89: SQL Injection\n- Related: CVE-2026-27461 (RLIKE injection in Dependency/Dao.php — different code path)\n\n\n---\n\n## Suggested Fix\n\nIn `TranslationController.php`: (1) Add allowlist check for non-language fieldnames before processing. (2) Replace raw string interpolation `UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))` with `$db->quoteIdentifier($fieldname)` to prevent SQL injection in date filter expressions.\n\n```diff\n--- a/src/Controller/Admin/TranslationController.php\n+++ b/src/Controller/Admin/TranslationController.php\n@@ -569,7 +569,15 @@ class TranslationController extends AdminAbstractController\n $fieldname = str_replace('--', '', $fieldname);\n \n if (!$languageMode && in_array($fieldname, $validLanguages)\n || $languageMode && !in_array($fieldname, $validLanguages)) {\n continue;\n }\n \n+ // Allowlist non-language fieldnames to prevent SQL injection\n+ $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate'];\n+ if (!$languageMode && !in_array($fieldname, $allowedNonLanguageFields) && !in_array($fieldname, $validLanguages)) {\n+ continue;\n+ }\n+\n if (!$languageMode) {\n $fieldname = $tableName . '.' . $fieldname;\n }\n@@ -582,7 +590,7 @@ class TranslationController extends AdminAbstractController\n } elseif ($filter[$operatorField] == 'eq') {\n $operator = '=';\n- $fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\";\n+ // Use validated fieldname only — never interpolate raw user input into SQL functions\n+ $fieldname = sprintf('UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(%s)))', $db->quoteIdentifier($fieldname));\n }\n\n```\n\n---\n\n## Proposed Fix\n\n```diff\n--- a/src/Controller/Admin/TranslationController.php\n+++ b/src/Controller/Admin/TranslationController.php\n@@ -569,7 +569,15 @@ class TranslationController extends AdminAbstractController\n $fieldname = str_replace('--', '', $fieldname);\n \n if (!$languageMode && in_array($fieldname, $validLanguages)\n || $languageMode && !in_array($fieldname, $validLanguages)) {\n continue;\n }\n \n+ // Allowlist non-language fieldnames to prevent SQL injection\n+ $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate'];\n+ if (!$languageMode && !in_array($fieldname, $allowedNonLanguageFields) && !in_array($fieldname, $validLanguages)) {\n+ continue;\n+ }\n+\n if (!$languageMode) {\n $fieldname = $tableName . '.' . $fieldname;\n }\n@@ -582,7 +590,7 @@ class TranslationController extends AdminAbstractController\n } elseif ($filter[$operatorField] == 'eq') {\n $operator = '=';\n- $fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\";\n+ // Use validated fieldname only — never interpolate raw user input into SQL functions\n+ $fieldname = sprintf('UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(%s)))', $db->quoteIdentifier($fieldname));\n }\n```\n\nHappy to submit this as a PR against a private fork if that is the preferred workflow.",
"details": "# GM-369\n\n## Summary\nSQL injection in Pimcore's translation grid date filter — the user-supplied `property` field from the filter JSON is interpolated directly into a `UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(...)))` SQL expression without parameterization or allowlist validation.\n\n## Affected Component\n- **Package:** `pimcore/admin-ui-classic-bundle`\n- **File:** `src/Controller/Admin/TranslationController.php`\n- **Lines:** 565 (input), 569 (inadequate sanitization), 593 (injection point)\n- **Endpoint:** `POST /admin/translation/translations`\n\n## Description\nThe translation grid endpoint processes JSON filter parameters. When a filter has `type: \"date\"`, the `property` field is extracted and used to construct a SQL expression:\n\n```php\n$fieldname = $filter[$propertyField]; // Line 565 — user input\n$fieldname = str_replace('--', '', $fieldname); // Line 569 — trivially bypassable\n$fieldname = $tableName . '.' . $fieldname; // Line 577\n$fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\"; // Line 593 — injection\n```\n\nThe `str_replace('--', '')` sanitization is trivially bypassable (use `/**/` comments or `----`). In non-language mode, `$fieldname` is concatenated directly into the SQL condition without quoting or parameterization.\n\n## Impact\nAuthenticated user with translations view permission can extract arbitrary database data via UNION-based or error-based SQL injection. Combined with GM-249 (unsafe unserialize), this enables an SQLi → deserialization → RCE chain.\n\n## Proof of Concept\n```\nPOST /admin/translation/translations\nfilter=[{\"property\":\"1))) UNION SELECT password FROM users WHERE ((1\",\"type\":\"date\",\"operator\":\"eq\",\"value\":\"2026-01-01\"}]\n```\n\n## Suggested Fix\nValidate `$fieldname` against an allowlist of valid column names before SQL interpolation:\n```php\n$allowedDateColumns = ['creationDate', 'modificationDate'];\nif (!in_array($fieldname, $allowedDateColumns, true)) {\n continue;\n}\n```\n\n## References\n- CWE-89: SQL Injection\n- Related: CVE-2026-27461 (RLIKE injection in Dependency/Dao.php — different code path)\n\n\n---\n\n## Suggested Fix\n\nIn `TranslationController.php`: (1) Add allowlist check for non-language fieldnames before processing. (2) Replace raw string interpolation `UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))` with `$db->quoteIdentifier($fieldname)` to prevent SQL injection in date filter expressions.\n\n```diff\n--- a/src/Controller/Admin/TranslationController.php\n+++ b/src/Controller/Admin/TranslationController.php\n@@ -569,7 +569,15 @@ class TranslationController extends AdminAbstractController\n $fieldname = str_replace('--', '', $fieldname);\n \n if (!$languageMode && in_array($fieldname, $validLanguages)\n || $languageMode && !in_array($fieldname, $validLanguages)) {\n continue;\n }\n \n+ // Allowlist non-language fieldnames to prevent SQL injection\n+ $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate'];\n+ if (!$languageMode && !in_array($fieldname, $allowedNonLanguageFields) && !in_array($fieldname, $validLanguages)) {\n+ continue;\n+ }\n+\n if (!$languageMode) {\n $fieldname = $tableName . '.' . $fieldname;\n }\n@@ -582,7 +590,7 @@ class TranslationController extends AdminAbstractController\n } elseif ($filter[$operatorField] == 'eq') {\n $operator = '=';\n- $fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\";\n+ // Use validated fieldname only — never interpolate raw user input into SQL functions\n+ $fieldname = sprintf('UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(%s)))', $db->quoteIdentifier($fieldname));\n }\n\n```\n\n---\n\n## Proposed Fix\n\n```diff\n--- a/src/Controller/Admin/TranslationController.php\n+++ b/src/Controller/Admin/TranslationController.php\n@@ -569,7 +569,15 @@ class TranslationController extends AdminAbstractController\n $fieldname = str_replace('--', '', $fieldname);\n \n if (!$languageMode && in_array($fieldname, $validLanguages)\n || $languageMode && !in_array($fieldname, $validLanguages)) {\n continue;\n }\n \n+ // Allowlist non-language fieldnames to prevent SQL injection\n+ $allowedNonLanguageFields = ['key', 'type', 'creationDate', 'modificationDate'];\n+ if (!$languageMode && !in_array($fieldname, $allowedNonLanguageFields) && !in_array($fieldname, $validLanguages)) {\n+ continue;\n+ }\n+\n if (!$languageMode) {\n $fieldname = $tableName . '.' . $fieldname;\n }\n@@ -582,7 +590,7 @@ class TranslationController extends AdminAbstractController\n } elseif ($filter[$operatorField] == 'eq') {\n $operator = '=';\n- $fieldname = \"UNIX_TIMESTAMP(DATE(FROM_UNIXTIME({$fieldname})))\";\n+ // Use validated fieldname only — never interpolate raw user input into SQL functions\n+ $fieldname = sprintf('UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(%s)))', $db->quoteIdentifier($fieldname));\n }\n```\n\nHappy to submit this as a PR against a private fork if that is the preferred workflow.",
"severity": [
{
"type": "CVSS_V3",
Expand Down Expand Up @@ -36,6 +36,28 @@
"database_specific": {
"last_known_affected_version_range": "<= 2.3.5"
}
},
{
"package": {
"ecosystem": "Packagist",
"name": "pimcore/admin-ui-classic-bundle"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "1.7.18"
}
]
}
],
"database_specific": {
"last_known_affected_version_range": "<= 1.7.17"
Comment on lines 50 to +59
}
}
],
"references": [
Expand Down