VYPR
High severityNVD Advisory· Published Feb 26, 2025· Updated Mar 12, 2025

Improper Authorization in Reporting API

CVE-2024-47053

Description

This advisory addresses an authorization vulnerability in Mautic's HTTP Basic Authentication implementation. This flaw could allow unauthorized access to sensitive report data.

  • Improper Authorization: An authorization flaw exists in Mautic's API Authorization implementation. Any authenticated user, regardless of assigned roles or permissions, can access all reports and their associated data via the API. This bypasses the intended access controls governed by the "Reporting Permissions > View Own" and "Reporting Permissions > View Others" permissions, which should restrict access to non-System Reports.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mautic/corePackagist
>= 1.0.1, < 5.2.35.2.3

Affected products

1

Patches

1
9d7ee57c9250

Merge pull request from GHSA-8xv7-g2q3-fqgc

https://github.com/mautic/mauticNick VanpraetFeb 25, 2025via ghsa
2 files changed · +161 65
  • app/bundles/ReportBundle/Controller/Api/ReportApiController.php+21 5 modified
    @@ -9,6 +9,8 @@
     use Mautic\CoreBundle\Factory\ModelFactory;
     use Mautic\CoreBundle\Helper\AppVersion;
     use Mautic\CoreBundle\Helper\CoreParametersHelper;
    +use Mautic\CoreBundle\Helper\UserHelper;
    +use Mautic\CoreBundle\Security\Exception\PermissionException;
     use Mautic\CoreBundle\Security\Permissions\CorePermissions;
     use Mautic\CoreBundle\Translation\Translator;
     use Mautic\ReportBundle\Entity\Report;
    @@ -30,7 +32,7 @@ class ReportApiController extends CommonApiController
          */
         protected $model;
     
    -    public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper, MauticFactory $factory)
    +    public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper, MauticFactory $factory, protected UserHelper $userHelper)
         {
             $reportModel = $modelFactory->getModel('report');
             \assert($reportModel instanceof ReportModel);
    @@ -48,17 +50,31 @@ public function __construct(CorePermissions $security, Translator $translator, E
          * Obtains a compiled report.
          *
          * @param int $id Report ID
    -     *
    -     * @return Response
          */
    -    public function getEntityAction(Request $request, $id)
    +    public function getEntityAction(Request $request, $id): Response
         {
    -        $entity = $this->model->getEntity($id);
    +        try {
    +            if (!$this->security->isGranted($this->permissionBase.':view')) {
    +                return $this->accessDenied();
    +            }
    +        } catch (PermissionException $e) {
    +            return $this->accessDenied($e->getMessage());
    +        }
    +
    +        $entity        = $this->model->getEntity($id);
     
             if (!$entity instanceof $this->entityClass) {
                 return $this->notFound();
             }
     
    +        if (
    +            $this->security->checkPermissionExists($this->permissionBase.':viewother')
    +            && !$this->security->isGranted($this->permissionBase.':viewother')
    +            && $entity->getCreatedBy() !== $this->userHelper->getUser()->getId()
    +        ) {
    +            return $this->accessDenied();
    +        }
    +
             $reportData = $this->model->getReportData($entity, $this->formFactory, $this->getOptionsFromRequest($request));
     
             // Unset keys that we don't need to send back
    
  • app/bundles/ReportBundle/Tests/Controller/Api/ReportApiControllerTest.php+140 60 modified
    @@ -3,73 +3,153 @@
     namespace Mautic\ReportBundle\Tests\Controller\Api;
     
     use Mautic\CoreBundle\Test\MauticMysqlTestCase;
    +use Mautic\ReportBundle\Entity\Report;
    +use Mautic\UserBundle\Entity\Permission;
    +use Mautic\UserBundle\Entity\Role;
    +use Mautic\UserBundle\Entity\User;
    +use Mautic\UserBundle\Model\RoleModel;
     use Symfony\Component\HttpFoundation\Response;
     
    -final class ReportApiControllerTest extends MauticMysqlTestCase
    +class ReportApiControllerTest extends MauticMysqlTestCase
     {
         protected $useCleanupRollback = false;
     
    +    public function testGetReportFailByNoCorrectAccessRoleEmpty(): void
    +    {
    +        $reportId = $this->createReportStructure('Maut1cR0cks!!!!!', []);
    +        $this->client->request('GET', '/api/reports/'.$reportId);
    +        $this->assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
    +    }
    +
    +    public function testGetReportSuccessByCorrectAccessIsAdmin(): void
    +    {
    +        $reportId = $this->createReportStructure('Maut1cR0cks!!!!!', [], false, true);
    +        $this->client->request('GET', '/api/reports/'.$reportId);
    +        $this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
    +    }
    +
    +    public function testGetReportSuccessByNoCorrectAccessToViewOther(): void
    +    {
    +        $reportId = $this->createReportStructure('Maut1cR0cks!!!!!', ['report:reports'=>['viewother']]);
    +        $this->client->request('GET', '/api/reports/'.$reportId);
    +        $this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
    +    }
    +
    +    public function testReportFailByNoCorrectAccessToViewOwn(): void
    +    {
    +        $reportId = $this->createReportStructure('Maut1cR0cks!!!!!', ['report:reports'=>['viewown']]);
    +        $this->client->request('GET', '/api/reports/'.$reportId);
    +        $this->assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
    +    }
    +
    +    public function testReportSuccessViewOwnBySameUser(): void
    +    {
    +        $reportId = $this->createReportStructure('Maut1cR0cks!!!!!', ['report:reports'=>['viewown']], true);
    +        $this->client->request('GET', '/api/reports/'.$reportId);
    +        $this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
    +    }
    +
         /**
    -     * Testing in a single method to decrease execution time from DB overhead.
    +     * @param array<array<string>> $permissions
          */
    -    public function testPostGetPatchPutDeleteEndPoints(): void
    +    private function createReportStructure(string $password, array $permissions, bool $createBy = false, bool $userIsAdmin = false): int
         {
    -        // Create a new report
    -        $data = json_decode(file_get_contents(__DIR__.'/data/post.json'), true);
    -        $this->client->request('POST', '/api/reports/new', $data);
    -        $response     = $this->client->getResponse();
    -        $responseData = json_decode($response->getContent(), true);
    -        $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode());
    -        $this->assertTrue(isset($responseData['report']));
    -        $this->assertEquals($data['name'], $responseData['report']['name']);
    -        $id     = $responseData['report']['id'];
    -        $source = $data['source'];
    -
    -        // Get the new report
    -        $this->client->restart();
    -        $this->client->request('GET', sprintf('/api/reports/%s', $id));
    -        $response = $this->client->getResponse();
    -        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    -        $responseData = json_decode($response->getContent(), true);
    -        $this->assertTrue(isset($responseData['data']));
    -        $this->assertTrue(isset($responseData['dataColumns']));
    -        $this->assertTrue(isset($responseData['report']));
    -        $this->assertEquals($data['name'], $responseData['report']['name']);
    -
    -        // Patch a report
    -        $data = json_decode(file_get_contents(__DIR__.'/data/patch.json'), true);
    -        $this->client->request('PATCH', sprintf('/api/reports/%s/edit', $id), $data);
    -        $response = $this->client->getResponse();
    -        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    -        $responseData = json_decode($response->getContent(), true);
    -        $this->assertTrue(isset($responseData['report']));
    -        $this->assertEquals($source, $responseData['report']['source']);
    -        $this->assertEquals($data['scheduleUnit'], $responseData['report']['scheduleUnit']);
    -        $this->assertEquals($data['toAddress'], $responseData['report']['toAddress']);
    -        $this->assertEquals($data['scheduleDay'], $responseData['report']['scheduleDay']);
    -
    -        // PUT a report
    -        $data = json_decode(file_get_contents(__DIR__.'/data/put.json'), true);
    -        $this->client->request('PUT', sprintf('/api/reports/%s/edit', $id), $data);
    -        $response = $this->client->getResponse();
    -        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    -        $responseData = json_decode($response->getContent(), true);
    -        $this->assertTrue(isset($responseData['report']));
    -        $this->assertEquals($data['name'], $responseData['report']['name']);
    -        $this->assertEquals($data['source'], $responseData['report']['source']);
    -        $this->assertEquals($data['scheduleUnit'], $responseData['report']['scheduleUnit']);
    -        $this->assertEquals($data['toAddress'], $responseData['report']['toAddress']);
    -        $this->assertEquals($data['scheduleDay'], $responseData['report']['scheduleDay']);
    -        $this->assertEmpty($responseData['report']['filters']);
    -
    -        // DELETE a report
    -        $this->client->request('DELETE', sprintf('/api/reports/%s/delete', $id), $data);
    -        $response = $this->client->getResponse();
    -        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
    -        $this->assertTrue(isset($responseData['report']));
    -        $this->assertEquals($data['name'], $responseData['report']['name']);
    -        $this->client->request('GET', sprintf('/api/reports/%s', $id), $data);
    -        $response = $this->client->getResponse();
    -        $this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
    +        $role           = $this->createRole($userIsAdmin);
    +        $user           = $this->createUser($role, $password);
    +        $createByIdUser = 0;
    +        if (!empty($createBy)) {
    +            $createByIdUser = $user->getId();
    +        }
    +        $report   = $this->createReportData($createByIdUser);
    +
    +        if ($permissions) {
    +            $this->setPermission($user, $permissions);
    +        }
    +        // Disable the default logging in via username and password.
    +        $this->clientServer = [];
    +        $this->setUpSymfony($this->configParams);
    +        $this->loginUser($user->getUserIdentifier());
    +        $this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
    +        $this->client->setServerParameter('PHP_AUTH_PW', $password);
    +
    +        return $report->getId();
    +    }
    +
    +    /**
    +     * @param array<array<string>> $permissions
    +     */
    +    private function setPermission(User $user, array $permissions): Role
    +    {
    +        $role = $user->getRole();
    +        // Delete previous permissions
    +        $this->em->createQueryBuilder()
    +            ->delete(Permission::class, 'p')
    +            ->where('p.bundle = :bundle')
    +            ->andWhere('p.role = :role_id')
    +            ->setParameters(['bundle' => 'report', 'role_id' => $role->getId()])
    +            ->getQuery()
    +            ->execute();
    +
    +        // Set new permissions
    +        $role->setIsAdmin(false);
    +        $roleModel = static::getContainer()->get('mautic.user.model.role');
    +        \assert($roleModel instanceof RoleModel);
    +        $roleModel->setRolePermissions($role, $permissions);
    +        $this->em->persist($role);
    +        $this->em->flush();
    +
    +        return $role;
    +    }
    +
    +    private function createUser(Role $role, string $password='mautic'): User
    +    {
    +        $user = new User();
    +        $user->setFirstName('John');
    +        $user->setLastName('Doe');
    +        $user->setUsername('john.doe');
    +        $user->setEmail('john.doe@email.com');
    +        $encoder = static::getContainer()->get('security.encoder_factory')->getEncoder($user);
    +        $user->setPassword($encoder->encodePassword($password, null));
    +        $user->setRole($role);
    +
    +        $this->em->persist($user);
    +        $this->em->flush();
    +
    +        return $user;
    +    }
    +
    +    private function createRole(bool $isAdmin = false): Role
    +    {
    +        $role = new Role();
    +        $role->setName('Role');
    +        $role->setIsAdmin($isAdmin);
    +
    +        $this->em->persist($role);
    +        $this->em->flush();
    +
    +        return $role;
    +    }
    +
    +    private function createReportData(int $createBy = 0): Report
    +    {
    +        $report = new Report();
    +        $report->setName('Contact report');
    +        $report->setDescription('<b>This is allowed HTML</b>');
    +        $report->setSource('leads');
    +        $coulmns = [
    +            'l.firstname',
    +            'l.lastname',
    +            'l.email',
    +            'l.date_added',
    +        ];
    +        $report->setColumns($coulmns);
    +        if (!empty($createBy)) {
    +            $report->setCreatedBy($createBy);
    +            $report->setCreatedByUser($createBy);
    +        }
    +
    +        $this->getContainer()->get('mautic.report.model.report')->saveEntity($report);
    +
    +        return $report;
         }
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.