VYPR
Moderate severityNVD Advisory· Published Mar 4, 2026· Updated Mar 4, 2026

Craft Affected by Entries Authorship Spoofing via Mass Assignment

CVE-2026-28781

Description

Craft is a content management system (CMS). Prior to 4.17.0-beta.1 and 5.9.0-beta.1, the entry creation process allows for Mass Assignment of the authorId attribute. A user with "Create Entries" permission can inject the authorIds[] (or authorId) parameter into the POST request, which the backend processes without verifying if the current user is authorized to assign authorship to others. Normally, this field is not present in the request for users without the necessary permissions. By manually adding this parameter, an attacker can attribute the new entry to any user, including Admins. This effectively "spoofs" the authorship. This vulnerability is fixed in 4.17.0-beta.1 and 5.9.0-beta.1.

AI Insight

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

Craft CMS 4.x and 5.x prior to beta fixes allow users with 'Create Entries' permission to spoof authorship by mass-assigning authorId in entry creation requests.

Vulnerability

Overview

Craft CMS, a popular content management system, is vulnerable to a mass assignment issue in its entry creation process. Prior to versions 4.17.0-beta.1 and 5.9.0-beta.1, the authorId attribute could be injected into a POST request during entry creation without proper server-side authorization checks [1][3]. The application normally does not include this field for users lacking the necessary permissions to assign authorship, but manually adding the parameter is not prevented [1]. This is a classic mass assignment vulnerability where the backend accepts user-supplied input to set a sensitive field without verifying the current user's right to modify it [3].

Attack

Vector and Prerequisites

To exploit this vulnerability, an attacker must have a user account with the "Create Entries" permission for a given section [3]. No additional privileges are required. The attacker intercepts the entry submission request (e.g., using a proxy tool like Burp Suite) and appends a parameter such as authorIds[] or authorId set to the ID of the target user [3]. The attacker must know the victim's user ID (e.g., ID 1 for the default admin) [3]. The request is then forwarded, and the backend processes the entry with the spoofed author attribution [3].

Impact

Successful exploitation allows the attacker to create entries whose authorship is attributed to any other user, including administrators [1][3]. This can be used to bypass review processes, attribute malicious or inappropriate content to trusted users, and undermine trust in the system [3]. The impact is primarily on the integrity of authorship metadata, and it may enable social engineering or reputation damage if the spoofed entries are visible to other site users [3].

Mitigation

The vulnerability has been fixed in Craft CMS versions 4.17.0-beta.1 and 5.9.0-beta.1 [1]. The fix adds validation rules for the authorId field, including a check that the assigned author has permission to author entries in the section, as shown in the commit diff [2]. Users should upgrade to these or later versions to remediate the issue.

AI Insight generated on May 18, 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
craftcms/cmsPackagist
>= 5.0.0-RC1, < 5.9.0-beta.15.9.0-beta.1
craftcms/cmsPackagist
>= 4.0.0-RC1, < 4.17.0-beta.14.17.0-beta.1

Affected products

2

Patches

2
830b403870cd

Fixed GHSA-2xfc-g69j-x2mp, part 2

https://github.com/craftcms/cmsbrandonkellyJan 21, 2026via ghsa
2 files changed · +2 1
  • CHANGELOG.md+1 0 modified
    @@ -4,6 +4,7 @@
     
     - Fixed an error that could occur when loading elements with provisional changes.
     - Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) user account enumeration vulnerability.
    +- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) permission escalation vulnerability.
     
     ## 5.9.0-beta.1 - 2026-01-20
     
    
  • src/controllers/ElementIndexesController.php+1 1 modified
    @@ -582,7 +582,7 @@ public function actionSaveElements(): Response
                 if (!empty($attributes)) {
                     $scenario = $element->getScenario();
                     $element->setScenario(Element::SCENARIO_LIVE);
    -                $element->setAttributes($attributes);
    +                $element->setAttributesFromRequest($attributes);
                     $element->setScenario($scenario);
                 }
     
    
c6dcbdffaf6a

Merge branch '4.17' into ghsa-jxm3-pmm2-9gf6/advisory-fix-v4

https://github.com/craftcms/cmsbrandonkellyJan 14, 2026via ghsa
7 files changed · +89 35
  • CHANGELOG-WIP.md+2 1 modified
    @@ -14,6 +14,7 @@
     - The `@parseRefs` and `@transform` GraphQL directives are now optional for each GraphQL schema. (GHSA-7x43-mpfg-r9wj)
     
     ### Extensibility
    +- Added `craft\base\ElementInterface::setAttributesFromRequest()`.
     - Added `craft\services\Search::deleteOrphanedIndexJobs()`.
     - Added `craft\web\GqlResponseFormatter`.
     - Added `craft\web\Response::FORMAT_GQL`.
    @@ -37,4 +38,4 @@
     - Fixed a bug where Table fields with the “Static Rows” setting enabled would lose track of which values belonged to which row headings, if the “Default Values” table was reordered. ([#17090](https://github.com/craftcms/cms/issues/17090))
     - Fixed a bug where deadlocks could occur when updating elements’ search indexes. ([#18139](https://github.com/craftcms/cms/pull/18139))
     - Fixed an RCE vulnerability. (GHSA-v47q-jxvr-p68x)
    -- Fixed a permission escalation vulnerability. (GHSA-jxm3-pmm2-9gf6)
    +- Fixed permission escalation vulnerabilities. (GHSA-2xfc-g69j-x2mp, GHSA-jxm3-pmm2-9gf6)
    
  • src/base/ElementInterface.php+8 0 modified
    @@ -1224,6 +1224,14 @@ public function isNextSiblingOf(self $element): bool;
          */
         public function offsetExists($offset): bool;
     
    +    /**
    +     * Sets the element’s attributes from an element editor submission.
    +     *
    +     * @param array $values The attribute values
    +     * @since 4.17.0
    +     */
    +    public function setAttributesFromRequest(array $values): void;
    +
         /**
          * Returns the status of a given attribute.
          *
    
  • src/base/Element.php+8 0 modified
    @@ -4008,6 +4008,14 @@ public function offsetExists($offset): bool
             return $offset === 'title' || $this->hasEagerLoadedElements($offset) || parent::offsetExists($offset) || $this->fieldByHandle($offset);
         }
     
    +    /**
    +     * @inheritdoc
    +     */
    +    public function setAttributesFromRequest(array $values): void
    +    {
    +        $this->setAttributes($values);
    +    }
    +
         /**
          * @inheritdoc
          */
    
  • src/controllers/ElementsController.php+2 2 modified
    @@ -199,7 +199,7 @@ public function actionCreate(): Response
             if ($this->_siteId) {
                 $element->siteId = $this->_siteId;
             }
    -        $element->setAttributes($this->_attributes);
    +        $element->setAttributesFromRequest($this->_attributes);
     
             $user = static::currentUser();
     
    @@ -2113,7 +2113,7 @@ private function _applyParamsToElement(ElementInterface $element): void
     
             $scenario = $element->getScenario();
             $element->setScenario(Element::SCENARIO_LIVE);
    -        $element->setAttributes($this->_attributes);
    +        $element->setAttributesFromRequest($this->_attributes);
     
             if ($this->_slug !== null) {
                 $element->slug = $this->_slug;
    
  • src/controllers/EntriesController.php+8 18 modified
    @@ -432,14 +432,14 @@ private function _getEntryModel(): Entry
         private function _populateEntryModel(Entry $entry): void
         {
             // Set the entry attributes, defaulting to the existing values for whatever is missing from the post data
    -        $entry->typeId = $this->request->getBodyParam('typeId', $entry->typeId);
    -        $entry->slug = $this->request->getBodyParam('slug', $entry->slug);
    -        if (($postDate = $this->request->getBodyParam('postDate')) !== null) {
    -            $entry->postDate = DateTimeHelper::toDateTime($postDate) ?: null;
    -        }
    -        if (($expiryDate = $this->request->getBodyParam('expiryDate')) !== null) {
    -            $entry->expiryDate = DateTimeHelper::toDateTime($expiryDate) ?: null;
    -        }
    +        $entry->setAttributesFromRequest(array_filter([
    +            'authorId' => $this->request->getBodyParam('author') ?? $entry->getAuthorId() ?? static::currentUser()->id,
    +            'expiryDate' => $this->request->getBodyParam('expiryDate'),
    +            'postDate' => $this->request->getBodyParam('postDate'),
    +            'slug' => $this->request->getBodyParam('slug'),
    +            'title' => $this->request->getBodyParam('title'),
    +            'typeId' => $this->request->getBodyParam('typeId'),
    +        ], fn($value) => $value !== null));
     
             $enabledForSite = $this->enabledForSiteValue();
             if (is_array($enabledForSite)) {
    @@ -449,7 +449,6 @@ private function _populateEntryModel(Entry $entry): void
                 $entry->enabled = (bool)$this->request->getBodyParam('enabled', $entry->enabled);
             }
             $entry->setEnabledForSite($enabledForSite ?? $entry->getEnabledForSite());
    -        $entry->title = $this->request->getBodyParam('title', $entry->title);
     
             if (!$entry->typeId) {
                 // Default to the section's first entry type
    @@ -462,15 +461,6 @@ private function _populateEntryModel(Entry $entry): void
             $fieldsLocation = $this->request->getParam('fieldsLocation', 'fields');
             $entry->setFieldValuesFromRequest($fieldsLocation);
     
    -        // Author
    -        $authorId = $this->request->getBodyParam('author', ($entry->authorId ?: static::currentUser()->id));
    -
    -        if (is_array($authorId)) {
    -            $authorId = $authorId[0] ?? null;
    -        }
    -
    -        $entry->authorId = $authorId;
    -
             // Parent
             if (($parentId = $this->request->getBodyParam('parentId')) !== null) {
                 $entry->setParentId($parentId);
    
  • src/elements/Entry.php+60 14 modified
    @@ -800,8 +800,10 @@ public function extraFields(): array
         public function attributeLabels(): array
         {
             return array_merge(parent::attributeLabels(), [
    -            'postDate' => Craft::t('app', 'Post Date'),
    +            'authorId' => Craft::t('app', 'Author'),
                 'expiryDate' => Craft::t('app', 'Expiry Date'),
    +            'postDate' => Craft::t('app', 'Post Date'),
    +            'typeId' => Craft::t('app', 'Entry Type'),
             ]);
         }
     
    @@ -812,6 +814,7 @@ protected function defineRules(): array
         {
             $rules = parent::defineRules();
             $rules[] = [['sectionId', 'typeId', 'authorId'], 'number', 'integerOnly' => true];
    +        $rules[] = [['sectionId', 'typeId'], 'required'];
             $rules[] = [['postDate', 'expiryDate'], DateTimeValidator::class];
     
             $rules[] = [
    @@ -825,17 +828,60 @@ protected function defineRules(): array
                 'on' => self::SCENARIO_LIVE,
             ];
     
    -        if ($this->sectionId) {
    -            $section = $this->getSection();
    +        $rules[] = [
    +            ['authorId'],
    +            'required',
    +            'when' => fn() => $this->getSection()->type !== Section::TYPE_SINGLE,
    +            'on' => self::SCENARIO_LIVE,
    +        ];
     
    -            if ($section->type !== Section::TYPE_SINGLE) {
    -                $rules[] = [['authorId'], 'required', 'on' => self::SCENARIO_LIVE];
    -            }
    -        }
    +        $rules[] = [
    +            ['typeId'],
    +            function($attribute) {
    +                $typeId = $this->getTypeId();
    +                foreach ($this->getAvailableEntryTypes() as $entryType) {
    +                    if ($entryType->id === $typeId) {
    +                        return;
    +                    }
    +                }
    +                $this->addError($attribute, Craft::t('yii', '{attribute} is invalid.', [
    +                    'attribute' => $this->getAttributeLabel($attribute),
    +                ]));
    +            },
    +        ];
    +
    +        $rules[] = [
    +            ['authorId'],
    +            function($attribute) {
    +                if (!$this->getAuthor()->can(sprintf("viewEntries:%s", $this->getSection()->uid))) {
    +                    $this->addError($attribute, Craft::t('app', 'This user doesn’t have permission to author entries in this section.'));
    +                }
    +            },
    +            'when' => fn() => $this->getSection()->type !== Section::TYPE_SINGLE,
    +        ];
     
             return $rules;
         }
     
    +    /**
    +     * @inheritdoc
    +     */
    +    public function setAttributesFromRequest(array $values): void
    +    {
    +        $authorId = $this->normalizeAuthorId(ArrayHelper::remove($values, 'authorId'));
    +
    +        parent::setAttributesFromRequest($values);
    +
    +        // Only set the author if the user has permission to change it
    +        if (
    +            $authorId !== null &&
    +            $authorId !== $this->getAuthorId() &&
    +            Craft::$app->getUser()->checkPermission(sprintf('viewPeerEntries:%s', $this->getSection()->uid))
    +        ) {
    +            $this->setAuthorId($authorId);
    +        }
    +    }
    +
         /**
          * @inheritdoc
          */
    @@ -1229,17 +1275,17 @@ public function getAuthorId(): ?int
          */
         public function setAuthorId(array|int|string|null $authorId): void
         {
    -        if ($authorId === '') {
    -            $authorId = null;
    -        }
    +        $this->_authorId = $this->normalizeAuthorId($authorId);
    +        $this->_author = null;
    +    }
     
    +    private function normalizeAuthorId(array|int|string|null $authorId): ?int
    +    {
             if (is_array($authorId)) {
    -            $this->_authorId = reset($authorId) ?: null;
    -        } else {
    -            $this->_authorId = $authorId;
    +            $authorId = reset($authorId);
             }
     
    -        $this->_author = null;
    +        return $authorId ? (int)$authorId : null;
         }
     
         /**
    
  • src/translations/en/app.php+1 0 modified
    @@ -1586,6 +1586,7 @@
         'This tab is conditional' => 'This tab is conditional',
         'This update requires PHP {v1}, but your composer.json file is currently set to PHP {v2}.' => 'This update requires PHP {v1}, but your composer.json file is currently set to PHP {v2}.',
         'This update requires PHP {v1}, but your environment is currently running PHP {v2}.' => 'This update requires PHP {v1}, but your environment is currently running PHP {v2}.',
    +    'This user doesn’t have permission to author entries in this section.' => 'This user doesn’t have permission to author entries in this section.',
         'This week' => 'This week',
         'This year' => 'This year',
         'This {type} doesn’t have revisions.' => 'This {type} doesn’t have revisions.',
    

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

5

News mentions

0

No linked articles in our index yet.