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

Contao has improper privilege management for page and article fields

CVE-2025-57759

Description

Contao is an Open Source CMS. In versions starting from 5.3.0 and prior to 5.3.38 and 5.6.1, under certain conditions, back end users may be able to edit fields of pages and articles without having the necessary permissions. This issue has been patched in versions 5.3.38 and 5.6.1. There are no workarounds.

AI Insight

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

Contao CMS 5.3.0–5.3.38/5.6.1 allows back-end users to edit page/article fields without proper permissions due to a privilege management flaw.

Vulnerability

Overview

CVE-2025-57759 is a privilege management vulnerability in Contao, an open-source CMS. In versions 5.3.0 up to (but not including) 5.3.38 and 5.6.1, the PagePermissionVoter class failed to properly enforce permission checks when back-end users edited pages or articles. Under certain conditions, users could modify fields without having the necessary permissions [1][4].

Root

Cause and Attack Vector

The issue stems from incomplete authorization logic in the Contao core. A commit shows that the fix adds a preloadPageTypes method and ensures that page-type-related data is fetched before performing access checks [2]. Without this preloading, the canCreate and canRead methods could mistakenly grant edit access based on stale or missing cache data. An attacker who is already a back-end user (authenticated) may exploit this by interacting with the management interface under conditions where the permission cache is not properly populated.

Impact

Successful exploitation allows a back-end user to edit fields of pages and articles they should not have access to. This could lead to unauthorized modification of site content, potential privilege escalation, or integrity violations. The vulnerability does not grant arbitrary code execution but directly undermines the CMS role-based access controls [1][4].

Mitigation

The vulnerability has been patched in Contao versions 5.3.38 and 5.6.1. There are no known workarounds; administrators must upgrade to the patched releases. Neither the NVD nor the advisory indicates active exploitation in the wild as of the publication date [1][4].

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.3.0, < 5.3.385.3.38
contao/core-bundlePackagist
>= 5.4.0-RC1, < 5.6.15.6.1
contao/contaoPackagist
>= 5.3.0, < 5.3.385.3.38
contao/contaoPackagist
>= 5.4.0-RC1, < 5.6.15.6.1

Affected products

2
  • Range: >=5.3.0 <5.3.38 or >=5.6.x <5.6.1
  • contao/contaov5
    Range: >= 5.3.0, < 5.3.38

Patches

1
80ee7db12d55

Merge commit from fork

https://github.com/contao/contaoAndreas SchemppAug 28, 2025via ghsa
5 files changed · +72 28
  • core-bundle/config/services.yaml+1 0 modified
    @@ -975,6 +975,7 @@ services:
             arguments:
                 - '@contao.framework'
                 - '@security.access.decision_manager'
    +            - '@database_connection'
     
         contao.security.data_container.page_type_access_voter:
             class: Contao\CoreBundle\Security\Voter\DataContainer\PageTypeAccessVoter
    
  • core-bundle/src/EventListener/DataContainer/DefaultOperationsListener.php+1 1 modified
    @@ -246,7 +246,7 @@ private function copyChildrenCallback(string $table): \Closure
         private function toggleCallback(string $table, string $toggleField): \Closure
         {
             return function (DataContainerOperation $operation) use ($toggleField, $table): void {
    -            $new = [$toggleField => !($operation['record'][$toggleField] ?? false)];
    +            $new = [$toggleField => !($operation->getRecord()[$toggleField] ?? false)];
     
                 if (!$this->isGranted(UpdateAction::class, $table, $operation, $new)) {
                     // Do not use DataContainerOperation::disable() because it would not show the
    
  • core-bundle/src/Security/Voter/DataContainer/ArticleContentVoter.php+18 11 modified
    @@ -22,7 +22,10 @@
      */
     class ArticleContentVoter extends AbstractDynamicPtableVoter
     {
    -    private array $pageIds = [];
    +    /**
    +     * @var array<int, array{id: int, type: string}>
    +     */
    +    private array $pageMap = [];
     
         public function __construct(
             private readonly AccessDecisionManagerInterface $accessDecisionManager,
    @@ -35,7 +38,7 @@ public function reset(): void
         {
             parent::reset();
     
    -        $this->pageIds = [];
    +        $this->pageMap = [];
         }
     
         protected function getTable(): string
    @@ -53,20 +56,24 @@ protected function hasAccessToRecord(TokenInterface $token, string $table, int $
                 return false;
             }
     
    -        $pageId = $this->getPageId($id);
    +        $page = $this->getPage($id);
     
    -        return $pageId
    -            && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE], $pageId)
    -            && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_EDIT_ARTICLES], $pageId);
    +        return $page
    +            && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE], $page['id'])
    +            && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_EDIT_ARTICLES], $page['id'])
    +            && $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE_TYPE], $page['type']);
         }
     
    -    private function getPageId(int $articleId): int|null
    +    /**
    +     * @return array{id: int, type: string}|null
    +     */
    +    private function getPage(int $articleId): array|null
         {
    -        if (!\array_key_exists($articleId, $this->pageIds)) {
    -            $pid = $this->connection->fetchOne('SELECT pid FROM tl_article WHERE id=?', [$articleId]);
    -            $this->pageIds[$articleId] = false !== $pid ? (int) $pid : null;
    +        if (!\array_key_exists($articleId, $this->pageMap)) {
    +            $record = $this->connection->fetchAssociative('SELECT id, type FROM tl_page WHERE id=(SELECT pid FROM tl_article WHERE id=?)', [$articleId]);
    +            $this->pageMap[$articleId] = false !== $record ? $record : null;
             }
     
    -        return $this->pageIds[$articleId];
    +        return $this->pageMap[$articleId];
         }
     }
    
  • core-bundle/src/Security/Voter/DataContainer/PagePermissionVoter.php+51 5 modified
    @@ -20,6 +20,8 @@
     use Contao\CoreBundle\Security\DataContainer\ReadAction;
     use Contao\CoreBundle\Security\DataContainer\UpdateAction;
     use Contao\Database;
    +use Doctrine\DBAL\ArrayParameterType;
    +use Doctrine\DBAL\Connection;
     use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
     use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
     use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
    @@ -33,9 +35,15 @@ class PagePermissionVoter implements VoterInterface, CacheableVoterInterface, Re
     {
         private array $pagemountsCache = [];
     
    +    /**
    +     * @var array<int, string|false>
    +     */
    +    private array $pageTypeCache = [];
    +
         public function __construct(
             private readonly ContaoFramework $framework,
             private readonly AccessDecisionManagerInterface $accessDecisionManager,
    +        private readonly Connection $connection,
         ) {
         }
     
    @@ -52,6 +60,7 @@ public function supportsType(string $subjectType): bool
         public function reset(): void
         {
             $this->pagemountsCache = [];
    +        $this->pageTypeCache = [];
         }
     
         public function vote(TokenInterface $token, $subject, array $attributes): int
    @@ -99,6 +108,7 @@ private function canCreate(CreateAction $action, TokenInterface $token): bool
             // Check access to any page for the "new" operation.
             if (null === $action->getNewPid()) {
                 $pageIds = $this->getPagemounts($token);
    +            $this->preloadPageTypes($pageIds);
             } else {
                 $pageIds = [(int) $action->getNewPid()];
             }
    @@ -119,7 +129,7 @@ private function canCreate(CreateAction $action, TokenInterface $token): bool
     
         private function canRead(ReadAction $action, TokenInterface $token): bool
         {
    -        return $this->canAccessPage($token, $this->getCurrentPageId($action));
    +        return $this->canAccessPage($token, $this->getCurrentPageId($action), false);
         }
     
         private function canUpdate(UpdateAction $action, TokenInterface $token): bool
    @@ -156,7 +166,7 @@ private function canUpdate(UpdateAction $action, TokenInterface $token): bool
             unset($newRecord['pid'], $newRecord['sorting'], $newRecord['tstamp']);
     
             // Record was possibly only moved (pid, sorting), no need to check edit permissions
    -        if ([] === array_diff($newRecord, $action->getCurrent())) {
    +        if ([] === array_diff_assoc($newRecord, $action->getCurrent())) {
                 return true;
             }
     
    @@ -173,7 +183,7 @@ private function canDelete(DeleteAction $action, TokenInterface $token): bool
                 default => throw new \UnexpectedValueException('Unsupported data source "'.$action->getDataSource().'"'),
             };
     
    -        return $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE], $pageId)
    +        return $this->canAccessPage($token, $pageId)
                 && $this->accessDecisionManager->decide($token, [$permission], $pageId);
         }
     
    @@ -225,8 +235,44 @@ private function canChangeHierarchy(CreateAction|UpdateAction $action, TokenInte
             return $this->accessDecisionManager->decide($token, $attributes, $pageId);
         }
     
    -    private function canAccessPage(TokenInterface $token, int $pageId): bool
    +    private function canAccessPage(TokenInterface $token, int $pageId, bool $checkType = true): bool
    +    {
    +        if (!$this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE], $pageId)) {
    +            return false;
    +        }
    +
    +        if (!$checkType) {
    +            return true;
    +        }
    +
    +        return $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE_TYPE], $this->getPageType($pageId));
    +    }
    +
    +    private function getPageType(int $id): string
         {
    -        return $this->accessDecisionManager->decide($token, [ContaoCorePermissions::USER_CAN_ACCESS_PAGE], $pageId);
    +        if (!isset($this->pageTypeCache[$id])) {
    +            $this->pageTypeCache[$id] = $this->connection->fetchOne('SELECT type FROM tl_page WHERE id=?', [$id]);
    +        }
    +
    +        if (false === $this->pageTypeCache[$id]) {
    +            throw new \UnexpectedValueException('Page ID "'.$id.'" not found');
    +        }
    +
    +        return $this->pageTypeCache[$id];
    +    }
    +
    +    private function preloadPageTypes(array $pageIds): void
    +    {
    +        $pageIds = array_values(array_flip(array_diff_key(array_flip($pageIds), $this->pageTypeCache)));
    +
    +        if ([] === $pageIds) {
    +            return;
    +        }
    +
    +        $this->pageTypeCache += $this->connection->fetchAllKeyValue(
    +            'SELECT id, type FROM tl_page WHERE id IN (?)',
    +            [$pageIds],
    +            [ArrayParameterType::INTEGER],
    +        );
         }
     }
    
  • core-bundle/src/Security/Voter/DataContainer/PageTypeAccessVoter.php+1 11 modified
    @@ -54,20 +54,10 @@ protected function getTable(): string
     
         protected function hasAccess(TokenInterface $token, CreateAction|DeleteAction|ReadAction|UpdateAction $action): bool
         {
    -        return $this->validateAccessToPageType($token, $action)
    -            && $this->validateFirstLevelType($action)
    +        return $this->validateFirstLevelType($action)
                 && $this->validateRootType($action);
         }
     
    -    private function validateAccessToPageType(TokenInterface $token, CreateAction|DeleteAction|ReadAction|UpdateAction $action): bool
    -    {
    -        if ($action instanceof ReadAction) {
    -            return true;
    -        }
    -
    -        return $this->hasAccessToType($token, ContaoCorePermissions::USER_CAN_ACCESS_PAGE_TYPE, $action);
    -    }
    -
         private function validateFirstLevelType(CreateAction|DeleteAction|ReadAction|UpdateAction $action): bool
         {
             if ($action instanceof ReadAction || $action instanceof DeleteAction) {
    

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.