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.
| Package | Affected versions | Patched versions |
|---|---|---|
mautic/corePackagist | >= 1.0.1, < 5.2.3 | 5.2.3 |
Affected products
1- Range: >= 1.0.1
Patches
19d7ee57c9250Merge pull request from GHSA-8xv7-g2q3-fqgc
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- github.com/advisories/GHSA-8xv7-g2q3-fqgcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-47053ghsaADVISORY
- cwe.mitre.org/data/definitions/287.htmlghsaWEB
- docs.mautic.org/en/5.2/configuration/settings.htmlghsaWEB
- github.com/mautic/mautic/commit/9d7ee57c92502ef77cddb091011c5ffef14b11eeghsaWEB
- github.com/mautic/mautic/security/advisories/GHSA-8xv7-g2q3-fqgcghsaWEB
News mentions
0No linked articles in our index yet.