VYPR
Moderate severityNVD Advisory· Published Aug 28, 2025· Updated Aug 28, 2025

Contao has improper access control in the back end voters

CVE-2025-57758

Description

Contao is an Open Source CMS. In versions starting from 5.0.0 and prior to 5.3.38 and 5.6.1, the table access voter in the back end doesn't check if a user is allowed to access the corresponding module. This issue has been patched in versions 5.3.38 and 5.6.1. A workaround involves not relying solely on the voter and additionally to check USER_CAN_ACCESS_MODULE.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Contao CMS back end table access voter fails to check module permissions, allowing unauthorized data operations until patched in 5.3.38/5.6.1.

Vulnerability

The table access voter in Contao's back end, responsible for controlling access to data operations (create, read, update, delete), does not verify whether a user is permitted to access the corresponding back end module [1]. This missing check means the voter grants or denies access based solely on the table name, without consulting the broader module-level permissions that should gate visibility and actions for each administrative area. The flaw affects Contao versions 5.0.0 through 5.3.37 and 5.6.0 [1].

Exploitation

An authenticated back end user can potentially perform operations (such as CRUD actions) on tables whose associated module they are not authorized to access [2]. The attacker needs a valid back end session and some knowledge of the target table structure. The attack complexity is low, as the missing check is triggered automatically when the voter evaluates a request for a table prefix [4]. No special privileges beyond being a logged-in back end user are required, and no user interaction beyond the attacker's own actions is needed [4].

Impact

Successful exploitation allows an attacker to read, create, update, or delete records in DCA (Data Container Array) tables that belong to modules to which they have no legitimate access [1][2]. This can lead to unauthorized data exposure, manipulation of content, or disruption of administrative functions, compromising the confidentiality, integrity, and availability of the CMS instance [4].

Mitigation

The vendor has released patches in versions 5.3.38 and 5.6.1 that add the missing module permission check [1][2]. Users should update immediately. As a workaround, administrators can supplement the voter by explicitly checking the USER_CAN_ACCESS_MODULE permission in custom code [1]. The vulnerability is not listed on CISA's Known Exploited Vulnerabilities catalog as of publication.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
contao/core-bundlePackagist
>= 5.0.0, < 5.3.385.3.38
contao/core-bundlePackagist
>= 5.4.0-RC1, < 5.6.15.6.1
contao/contaoPackagist
>= 5.0.0, < 5.3.385.3.38
contao/contaoPackagist
>= 5.4.0-RC1, < 5.6.15.6.1

Affected products

2
  • Range: >=5.0.0, <5.3.38 || >=5.0.0, <5.6.1
  • contao/contaov5
    Range: >= 5.0.0, < 5.3.38

Patches

1
3f05c603e1c9

Merge commit from fork

https://github.com/contao/contaoAndreas SchemppAug 28, 2025via ghsa
3 files changed · +135 40
  • core-bundle/src/Security/Voter/DataContainer/AbstractDataContainerVoter.php+1 2 modified
    @@ -19,9 +19,8 @@
     use Contao\CoreBundle\Security\DataContainer\UpdateAction;
     use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
     use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
    -use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
     
    -abstract class AbstractDataContainerVoter implements VoterInterface, CacheableVoterInterface
    +abstract class AbstractDataContainerVoter implements CacheableVoterInterface
     {
         public function supportsAttribute(string $attribute): bool
         {
    
  • core-bundle/src/Security/Voter/DataContainer/TableAccessVoter.php+48 20 modified
    @@ -14,6 +14,8 @@
     
     use Contao\CoreBundle\Security\ContaoCorePermissions;
     use Contao\CoreBundle\Security\DataContainer\CreateAction;
    +use Contao\CoreBundle\Security\DataContainer\DeleteAction;
    +use Contao\CoreBundle\Security\DataContainer\ReadAction;
     use Contao\CoreBundle\Security\DataContainer\UpdateAction;
     use Contao\DataContainer;
     use Contao\DC_File;
    @@ -37,39 +39,65 @@ public function supportsAttribute(string $attribute): bool
     
         public function supportsType(string $subjectType): bool
         {
    -        return match ($subjectType) {
    -            CreateAction::class,
    -            UpdateAction::class => true,
    -            default => false,
    -        };
    +        return \in_array($subjectType, [CreateAction::class, ReadAction::class, UpdateAction::class, DeleteAction::class], true);
         }
     
         /**
          * @param CreateAction|UpdateAction $subject
          */
         public function vote(TokenInterface $token, $subject, array $attributes): int
         {
    -        foreach ($attributes as $attribute) {
    -            if (!$this->supportsAttribute($attribute) || DC_File::class === DataContainer::getDriverForTable($subject->getDataSource())) {
    -                continue;
    -            }
    +        if (!array_filter($attributes, $this->supportsAttribute(...))) {
    +            return self::ACCESS_ABSTAIN;
    +        }
     
    -            $hasNotExcluded = false;
    +        // Check access to a module with this DCA
    +        if (!$this->hasAccessToModule($token, $subject->getDataSource())) {
    +            return self::ACCESS_DENIED;
    +        }
     
    -            // Intentionally do not load DCA, it should already be loaded. If DCA is not
    -            // loaded, the voter just always abstains because it can't decide.
    -            foreach ($GLOBALS['TL_DCA'][$subject->getDataSource()]['fields'] ?? [] as $config) {
    -                if (!($config['exclude'] ?? true)) {
    -                    $hasNotExcluded = true;
    -                    break;
    -                }
    -            }
    +        // If table access is granted, we don't check field permission for READ or DELETE
    +        if ($subject instanceof ReadAction || $subject instanceof DeleteAction) {
    +            return self::ACCESS_ABSTAIN;
    +        }
    +
    +        // DC_File does not have excluded fields
    +        if (DC_File::class === DataContainer::getDriverForTable($subject->getDataSource())) {
    +            return self::ACCESS_ABSTAIN;
    +        }
    +
    +        $hasNotExcluded = false;
     
    -            if (!$hasNotExcluded && !$this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], $subject->getDataSource())) {
    -                return self::ACCESS_DENIED;
    +        // Intentionally do not load DCA, it should already be loaded. If DCA is not
    +        // loaded, the voter just always abstains because it can't decide.
    +        foreach ($GLOBALS['TL_DCA'][$subject->getDataSource()]['fields'] ?? [] as $config) {
    +            if (!($config['exclude'] ?? true)) {
    +                $hasNotExcluded = true;
    +                break;
                 }
             }
     
    +        if (!$hasNotExcluded && !$this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], $subject->getDataSource())) {
    +            return self::ACCESS_DENIED;
    +        }
    +
             return self::ACCESS_ABSTAIN;
         }
    +
    +    private function hasAccessToModule(TokenInterface $token, string $table): bool
    +    {
    +        foreach ($GLOBALS['BE_MOD'] as $modules) {
    +            foreach ($modules as $name => $config) {
    +                if (
    +                    \is_array($config['tables'] ?? null)
    +                    && \in_array($table, $config['tables'], true)
    +                    && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], $name)
    +                ) {
    +                    return true;
    +                }
    +            }
    +        }
    +
    +        return false;
    +    }
     }
    
  • core-bundle/tests/Security/Voter/DataContainer/TableAccessVoterTest.php+86 18 modified
    @@ -40,14 +40,14 @@ protected function setUp(): void
             $this->voter = new TableAccessVoter($this->accessDecisionManager);
             $this->token = $this->createMock(TokenInterface::class);
     
    -        unset($GLOBALS['TL_DCA']);
    +        unset($GLOBALS['TL_DCA'], $GLOBALS['TL_MOD']);
         }
     
         protected function tearDown(): void
         {
             parent::tearDown();
     
    -        unset($GLOBALS['TL_DCA']);
    +        unset($GLOBALS['TL_DCA'], $GLOBALS['TL_MOD']);
         }
     
         public function testSupportsDCAttribute(): void
    @@ -56,12 +56,12 @@ public function testSupportsDCAttribute(): void
             $this->assertFalse($this->voter->supportsAttribute('foobar'));
         }
     
    -    public function testSupportsCreateAndUpdateActionSubject(): void
    +    public function testSupportsCRUDActionSubject(): void
         {
             $this->assertTrue($this->voter->supportsType(CreateAction::class));
             $this->assertTrue($this->voter->supportsType(UpdateAction::class));
    -        $this->assertFalse($this->voter->supportsType(ReadAction::class));
    -        $this->assertFalse($this->voter->supportsType(DeleteAction::class));
    +        $this->assertTrue($this->voter->supportsType(ReadAction::class));
    +        $this->assertTrue($this->voter->supportsType(DeleteAction::class));
             $this->assertFalse($this->voter->supportsType('foobar'));
         }
     
    @@ -78,8 +78,58 @@ public function testAbstainsOnUnsupportedAttribute(): void
             );
         }
     
    +    public function testDeniesIfTableIsNotInAllowedModule(): void
    +    {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
    +        $this->accessDecisionManager
    +            ->expects($this->once())
    +            ->method('decide')
    +            ->with($this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article')
    +            ->willReturn(false)
    +        ;
    +
    +        $this->assertSame(
    +            VoterInterface::ACCESS_DENIED,
    +            $this->voter->vote($this->token, new CreateAction('tl_foobar'), [ContaoCorePermissions::DC_PREFIX.'tl_foobar']),
    +        );
    +    }
    +
    +    public function testAbstainsIfTableIsAllowedInSecondaryModule(): void
    +    {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +        $GLOBALS['BE_MOD']['foo']['bar']['tables'] = ['tl_foobar'];
    +
    +        $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
    +            'foo' => [
    +                'inputType' => 'text',
    +                'exclude' => true,
    +            ],
    +            'bar' => [
    +                'inputType' => 'text',
    +                'exclude' => false,
    +            ],
    +        ];
    +
    +        $this->accessDecisionManager
    +            ->expects($this->exactly(2))
    +            ->method('decide')
    +            ->willReturnMap([
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article', false],
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'bar', true],
    +            ])
    +        ;
    +
    +        $this->assertSame(
    +            VoterInterface::ACCESS_ABSTAIN,
    +            $this->voter->vote($this->token, new CreateAction('tl_foobar'), [ContaoCorePermissions::DC_PREFIX.'tl_foobar']),
    +        );
    +    }
    +
         public function testAbstainsIfExcludedFieldAccessIsGranted(): void
         {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
             $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
                 'foo' => [
                     'inputType' => 'text',
    @@ -88,10 +138,12 @@ public function testAbstainsIfExcludedFieldAccessIsGranted(): void
             ];
     
             $this->accessDecisionManager
    -            ->expects($this->once())
    +            ->expects($this->exactly(2))
                 ->method('decide')
    -            ->with($this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar')
    -            ->willReturn(true)
    +            ->willReturnMap([
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article', true],
    +                [$this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar', true],
    +            ])
             ;
     
             $this->assertSame(
    @@ -102,17 +154,21 @@ public function testAbstainsIfExcludedFieldAccessIsGranted(): void
     
         public function testAbstainsIfDefaultExcludedFieldAccessIsGranted(): void
         {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
             $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
                 'foo' => [
                     'inputType' => 'text',
                 ],
             ];
     
             $this->accessDecisionManager
    -            ->expects($this->once())
    +            ->expects($this->exactly(2))
                 ->method('decide')
    -            ->with($this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar')
    -            ->willReturn(true)
    +            ->willReturnMap([
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article', true],
    +                [$this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar', true],
    +            ])
             ;
     
             $this->assertSame(
    @@ -123,6 +179,8 @@ public function testAbstainsIfDefaultExcludedFieldAccessIsGranted(): void
     
         public function testAbstainsIfAtLeastOneFieldIsNotExcluded(): void
         {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
             $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
                 'foo' => [
                     'inputType' => 'text',
    @@ -135,8 +193,10 @@ public function testAbstainsIfAtLeastOneFieldIsNotExcluded(): void
             ];
     
             $this->accessDecisionManager
    -            ->expects($this->never())
    +            ->expects($this->once())
                 ->method('decide')
    +            ->with($this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article')
    +            ->willReturn(true)
             ;
     
             $this->assertSame(
    @@ -147,6 +207,8 @@ public function testAbstainsIfAtLeastOneFieldIsNotExcluded(): void
     
         public function testDeniesAccessIfFieldIsExcluded(): void
         {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
             $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
                 'foo' => [
                     'inputType' => 'text',
    @@ -159,10 +221,12 @@ public function testDeniesAccessIfFieldIsExcluded(): void
             ];
     
             $this->accessDecisionManager
    -            ->expects($this->once())
    +            ->expects($this->exactly(2))
                 ->method('decide')
    -            ->with($this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar')
    -            ->willReturn(false)
    +            ->willReturnMap([
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article', true],
    +                [$this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar', false],
    +            ])
             ;
     
             $this->assertSame(
    @@ -173,6 +237,8 @@ public function testDeniesAccessIfFieldIsExcluded(): void
     
         public function testDeniesAccessIfFieldIsDefaultExcluded(): void
         {
    +        $GLOBALS['BE_MOD']['content']['article']['tables'] = ['tl_foobar'];
    +
             $GLOBALS['TL_DCA']['tl_foobar']['fields'] = [
                 'foo' => [
                     'inputType' => 'text',
    @@ -184,10 +250,12 @@ public function testDeniesAccessIfFieldIsDefaultExcluded(): void
             ];
     
             $this->accessDecisionManager
    -            ->expects($this->once())
    +            ->expects($this->exactly(2))
                 ->method('decide')
    -            ->with($this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar')
    -            ->willReturn(false)
    +            ->willReturnMap([
    +                [$this->token, [ContaoCorePermissions::USER_CAN_ACCESS_MODULE], 'article', true],
    +                [$this->token, [ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE], 'tl_foobar', false],
    +            ])
             ;
     
             $this->assertSame(
    

Vulnerability mechanics

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

References

5

News mentions

0

No linked articles in our index yet.