GHSA-qq2c-2q8j-jh27
Fix: craftcms/cms@9cc493bGHSA-qq2c-2q8j-jh27 is a CWE-285 vulnerability in craftcms/cms. O3 Security confirms whether GHSA-qq2c-2q8j-jh27 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Craft CMS: Authorship spoofing in `entries/save-entry` via pre-check/post-mutation authorization gap
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-qq2c-2q8j-jh27.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
craftcms/cmsReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
EntriesController::actionSaveEntry() performs entry-edit permission checks before request-controlled author changes are applied to the model. The subsequent author mutation path accepts attacker-supplied authors / author parameters and allows the change when the current user is one of the old authors. Because the controller does not re-run authorization after mutating the author list, a low-privileged user can reassign an entry’s authorship to another user without holding the dedicated peer-author-change permission.
Details
The control flow begins in EntriesController.php:249. actionSaveEntry() loads the entry and enforces edit permissions before calling _populateEntryModel():
public function actionSaveEntry(bool $duplicate = false): ?Response
{
...
$entry = $this->_editableEntry($this->request->getBodyParam('entryId'), $siteId);
...
$this->enforceEditEntryPermissions($entry, $duplicate);
...
$this->_populateEntryModel($entry);
...
$success = Craft::$app->getElements()->saveElement($entry);
}
The attacker-controlled source is in EntriesController.php:588:
$entry->setAttributesFromRequest(array_filter([
'authorIds' => $this->request->getBodyParam('authors') ??
$this->request->getBodyParam('author') ??
$entry->getAuthorId() ??
static::currentUser()->id,
]));
Entry::setAttributesFromRequest() in Entry.php:1124 extracts the new author IDs and applies them if canChangeAuthor() returns true:
if (
($authorIds !== null || $authorId !== null) &&
$this->canChangeAuthor()
) {
$this->_oldAuthorIds = $oldAuthorIds;
$this->setAuthorIds($authorIds);
}
canChangeAuthor() at Entry.php:2789 allows the author change when the current user can view peer entries and is already one of the existing authors:
return (
empty($authorIds) ||
in_array($user->id, $authorIds) ||
$user->can("changeAuthorForPeerEntries:$section->uid")
);
After the author list is mutated, the controller does not re-check authorization.
This closes the exploit chain:
- External source: authenticated request to
entries/save-entrywith attacker-controlledauthors[]. - Trust boundary failure: authorization is checked on the pre-mutation entry state, not on the post-mutation author assignment.
- Privileged sink: the author relationship is rewritten in persistent storage.
Preconditions derived from the source:
- The attacker is authenticated and can edit entry
345. - The attacker is among the existing authors of entry
345, or otherwise satisfiescanChangeAuthor()through the old author set. - The attacker has
viewPeerEntriesfor the section. - User ID
1exists and can be assigned as an author in that section.
Result:
enforceEditEntryPermissions()succeeds on the original entry state._populateEntryModel()readsauthors[]=1from the request body.setAttributesFromRequest()updatesauthorIdsbecausecanChangeAuthor()is evaluated against the old authorship state.saveElement()persists the change and_saveAuthors()rewrites the entry-author relation.- Entry
345now appears authored by user1.
Impact
This allows low-privileged users to falsify content ownership and alter the authorship of entries without having the dedicated author-management permission. The impact includes corrupted audit trails, misleading notifications, broken approval workflows, and unauthorized reassignment of content responsibility.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | craftcms/cms | ≥ 5.0.0-RC1&&< 5.9.21 | 5.9.21 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for craftcms/cms. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update craftcms/cms to 5.9.21 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-qq2c-2q8j-jh27 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-qq2c-2q8j-jh27 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-qq2c-2q8j-jh27. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-qq2c-2q8j-jh27 in your dependencies?
O3 detects GHSA-qq2c-2q8j-jh27 across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.