Impact
This is an additional fix for https://github.com/craftcms/cms/security/advisories/GHSA-4w8r-3xrw-v25g
This is a high-impact, low-complexity attack vector. To mitigate the issue, users running Craft installations before the fixed versions are encouraged to update to at least that version.
Details
https://craftcms.com/knowledge-base/craft-cms-cve-2025-32432
References
https://github.com/craftcms/cms/commit/e1c85441fa47eeb7c688c2053f25419bc0547b47
https://github.com/craftcms/cms/blob/3.x/CHANGELOG.md#3915---2025-04-10-critical https://github.com/craftcms/cms/blob/4.x/CHANGELOG.md#41415---2025-04-10-critical https://github.com/craftcms/cms/blob/5.x/CHANGELOG.md#5617---2025-04-10-critical
https://sensepost.com/blog/2025/investigating-an-in-the-wild-campaign-using-rce-in-craftcms/
Credits
Credit to Orange Cyberdefense for discovering a reporting this bug.
Impact You are affected if your php.ini configuration has registerargcargv enabled.
Patches Update to 3.9.14, 4.13.2, or 5.5.2.
Workarounds If you can't upgrade yet, and registerargcargv is enabled, you can disable it to mitigate the issue.
Craft CMS 5.0.0-RC1 through 5.10.5 contains an incorrect authorization vulnerability. A control-panel user holding only the viewCategories permission (without saveCategories) for a category group can permanently modify that group's category structure — reordering and re-parenting categories — via the structures/move-element action. The structureEditable flag is computed from the view permission rather than the save permission, and the StructuresController authorizes the mutating action on that read-time session grant without a save re-check. Because a category's URI is derived from its position in the structure, moving a category changes its URL and those of its descendants and can corrupt navigation menus built from the category taxonomy. The issue is fixed in 5.10.6.
Craft CMS before 5.10.11 fails to validate the admin flag during user registration, allowing it to persist from deactivated admin accounts. Attackers can register with a deactivated admin's email address to inherit administrator privileges when public registration and disabled email verification are configured.
Craft CMS 4.8.0 through 4.18.5 and 5.0.0 through 5.10.12 sign an authenticated user's attacker-controlled license-shun cookie with the same key and format used to validate signed redirect parameters, because the HMAC signature is not bound to its purpose (Yii's cookieValidationKey is derived from the same Craft securityKey used for signed request parameters). An authenticated, non-administrator user (Control Panel access is not required) can set the cookie via the license-shun endpoint and transplant the signed envelope into the redirect parameter; on a successful login, Craft validates the signature and renders the authenticated bytes as an unsandboxed Twig template, where Twig's map filter accepts a string callback and allows PHP system() to execute arbitrary operating-system commands as the web-server user. Exploitation requires an account using password authentication without active 2FA, the default request configuration, and availability of PHP system(). The issue is fixed in 4.18.6 and 5.10.13.
Craft CMS versions 5.10.0 through 5.10.12 contain an incomplete fix for CVE-2026-55794: the Controller::getPostedRedirectUrl() -> View::renderObjectTemplate() sink remained unsandboxed, and the same fix commit added a self-signing oracle in Cp::elementLabelHtml(). Because Craft/Yii HMAC tokens are not bound to a parameter name, an authenticated low-privilege control panel user with edit rights on a single element type can mint a token over attacker-controlled Twig for the returnUrl parameter and replay it as the redirect POST parameter, reaching the unsandboxed sink and achieving server-side template injection that executes arbitrary PHP code (full server compromise). The issue is fixed in 5.10.13.
Requirements:
Control panel access Permissions to edit an entry
Details
Control panel users with the ability to edit entries can execute unsandboxed Twig code via the HTTP Referrer header.
The issue happens when a user is saving entries. Strings for a signed redirect URL are being compiled as a Twig template via renderObjectTemplate(), and while a sandboxed alternative already exists (renderSandboxedObjectTemplate()), it is not used in this case. This signed URL can be specified by users, as it is reflected in the “Referer” HTTP request header, which is under attacker control.
This has been fixed in Craft 5.10.0. Affected users should update to that version or higher to get the fix.
Resources
https://github.com/craftcms/cms/pull/18680
Craft CMS versions from 4.0.0-RC1 before 4.18.2 and from 5.0.0-RC1 before 5.10.6 contain an authenticated remote code execution vulnerability in the control panel element-search condition handling. Craft cleanses the outer request-controlled condition array via Component::cleanseConfig(), but Conditions::createCondition() later decodes and merges the JSON string in condition.config without re-running cleanseConfig() on the decoded configuration. Because condition.config is a JSON string during the first cleanse, Yii special config keys such as 'as ...' and 'on ...' can be hidden inside it and, after JSON decoding, are interpreted by Yii as behavior/event configuration during FieldLayout object creation. An attacker with an authenticated control panel session (and a valid CSRF token) can exploit this to execute operating system commands as the PHP/web user.
Craft CMS versions >= 5.0.0-RC1 before 5.10.7 and >= 4.0.0-RC1 before 4.18.3 contain a remote code execution vulnerability in the Twig sandbox mechanism. Because Craft marks the ElementInterface as safe (via the AllowedInSandbox attribute) and the sandbox allowlisting extends to the entire class hierarchy (craft\base\Component up to yii\base\Component), an authenticated attacker with permission to access the control panel can render a malicious Twig template that abuses the yii\base\Component arbitrary function-call gadget to execute arbitrary code, even when the Twig sandbox is enabled via enableTwigSandbox().
Craft CMS versions from 4.0.0-RC1 before 4.18.2 and from 5.0.0-RC1 before 5.10.6 contain an authenticated remote code execution vulnerability in control panel element-search condition handling. A JSON cleanse bypass in condition.config allows Yii behavior/event configuration keys to be interpreted after decoding, enabling command execution as the PHP/web user.
The Twig sandbox mechanism in Craft CMS is configured to allow dangerous functionality from the Yii framework, leading to authenticated RCE similar to previously disclosed vulnerabilities.
Craft CMS versions before 5.10.11 fail to validate admin status in the actionGetPasswordResetUrl endpoint, allowing non-admin users with administrateUsers permission to mint password reset URLs for administrator accounts. Attackers can generate a valid reset URL for any admin user and set a new password via actionSetPassword, which validates only the verification code without checking the caller's session, enabling complete control-panel takeover.
Craft CMS versions before 5.10.11 contain a site scope bypass vulnerability in GraphQL entry mutation resolvers that fail to validate siteId through ArgumentManager::prepareArguments(). Attackers with tokens scoped to one site can read, modify, or delete entries across unauthorized sites by passing siteId directly in mutation arguments.
Craft CMS versions before 5.10.12 contain a remote code execution vulnerability in the element-index endpoint that allows authenticated content editors to instantiate arbitrary classes through the criteria parameter. Attackers can inject a malicious class via criteria[withTransforms][0][class] that reaches ImageTransforms::normalizeTransform(), then use a PHP gadget chain with yii\rbac\PhpManager to execute code by pointing itemFile to a request log containing PHP payload in the User-Agent header.
Craft CMS versions before 5.10.12 fail to properly cleanse string-typed field-layout elements, allowing authenticated control-panel users to inject Yii2 behavior attachments and event handlers. Attackers can post field-layout tab elements as JSON strings to bypass cleanse validation, then trigger arbitrary object instantiation and code execution through Craft::createObject().
A remote, authenticated, non-admin Craft CMS Control Panel user with only the accessCp permission can execute operating system commands as the PHP web worker.
Impact
This is an RCE vulnerability that affects Craft 4 and 5 installs where your security key has already been compromised.
https://craftcms.com/knowledge-base/securing-craft#keep-your-secrets-secret
Anyone running an unpatched version of Craft with a compromised security key is affected.
Patches
This has been patched in Craft 5.5.8 and 4.13.8.
Workarounds
If you can't update to a patched version, then rotating your security key and ensuring its privacy will help to migitgate the issue.
References
https://github.com/craftcms/cms/commit/e59e22b30c9dd39e5e2c7fe02c147bcbd004e603
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():
php 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:
php $entry->setAttributesFromRequest(arrayfilter([ '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:
php 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:
php return ( empty($authorIds) || inarray($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:
1. External source: authenticated request to entries/save-entry with attacker-controlled authors[]. 2. Trust boundary failure: authorization is checked on the pre-mutation entry state, not on the post-mutation author assignment. 3. Privileged sink: the author relationship is rewritten in persistent storage.
Preconditions derived from the source:
1. The attacker is authenticated and can edit entry 345. 2. The attacker is among the existing authors of entry 345, or otherwise satisfies canChangeAuthor() through the old author set. 3. The attacker has viewPeerEntries for the section. 4. User ID 1 exists and can be assigned as an author in that section.
Result:
1. enforceEditEntryPermissions() succeeds on the original entry state. 2. populateEntryModel() reads authors[]=1 from the request body. 3. setAttributesFromRequest() updates authorIds because canChangeAuthor() is evaluated against the old authorship state. 4. saveElement() persists the change and saveAuthors() rewrites the entry-author relation. 5. Entry 345 now appears authored by user 1.
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.
Craft CMS 5.0.0-RC1 through versions before 5.11.0 incorrectly authorize the GraphQL draftCreator and revisionCreator fields: instead of requiring the user-data scope enforced by Gql::canQueryUsers() (usergroups.:read), these fields are gated only on the elements.drafts:read / elements.revisions:read scopes, and their resolver returns a raw User element whose email, username, fullName, and addresses fields have no per-field authorization. A client holding only the drafts or revisions scope — including an unauthenticated client when the operator has enabled the public GraphQL schema with those scopes — can therefore harvest the email addresses, usernames, full names, and postal addresses of all draft/revision creators (typically site editors and administrators). The issue is fixed in 5.11.0.
Summary
AssetsController::actionDeleteFolder() only requires the deleteAssets:<volume-uid> permission for the target folder. It never enforces deletePeerAssets:<volume-uid>, even though Assets::deleteFoldersByIds() cascades deletion to every descendant folder and every asset inside, regardless of who uploaded them. A low-privilege user who has been granted folder-management rights on a shared volume can therefore destroy assets uploaded by other users (peer assets), bypassing the per-asset peer-permission check that the sibling actionDeleteAsset endpoint correctly applies.
This is the same bug class that was just fixed in actionMoveFolder as GHSA-3w32-23wj-rxg3 (commit 05c2042, Apr 23 2026); the fix added requireVolumePermissionByFolder('deletePeerAssets', …) and savePeerAssets checks to the move endpoint but did not propagate to the delete-folder endpoint.
Details
src/controllers/AssetsController.php:552-569:
php public function actionDeleteFolder(): Response { $this->requireAcceptsJson(); $folderId = $this->request->getRequiredBodyParam('folderId');
$assets = Craft::$app->getAssets(); $folder = $assets->getFolderById($folderId);
if (!$folder) { throw new BadRequestHttpException('The folder cannot be found'); }
// Check if it's possible to delete objects in the target volume. $this->requireVolumePermissionByFolder('deleteAssets', $folder); // <-- only checks deleteAssets $assets->deleteFoldersByIds($folderId);
return $this->asSuccess(); }
requireVolumePermissionByFolder() (src/controllers/AssetsControllerTrait.php:75-88) only resolves to a single requirePermission('deleteAssets:<vol-uid>') call. The peer-equivalent helper (requirePeerVolumePermissionByAsset) is never invoked because there is no folder-level peer helper that iterates the folder's contents.
Assets::deleteFoldersByIds() (src/services/Assets.php:311-349) then enumerates the folder + every descendant folder, queries every asset under those IDs, and calls Craft::$app->getElements()->deleteElement($asset, true) directly:
php $assetQuery = Asset::find()->folderId($allFolderIds); $elementService = Craft::$app->getElements();
foreach (Db::each($assetQuery) as $asset) { $asset->keepFileOnDelete = !$deleteDir; $elementService->deleteElement($asset, true); }
This bypasses Asset::canDelete() (src/elements/Asset.php:1515-1536):
php public function canDelete(User $user): bool { if ($this->isFolder) { return false; } if (parent::canDelete($user)) { return true; } $volume = $this->getVolume(); if (Assets::isTempUploadFs($volume->getFs())) { return true; }
if ($this->uploaderId !== $user->id) { return $user->can("deletePeerAssets:$volume->uid"); // <-- never reached on cascade delete } return $user->can("deleteAssets:$volume->uid"); }
Compare to actionDeleteAsset (src/controllers/AssetsController.php:579-613), which correctly does:
php $this->requireVolumePermissionByAsset('deleteAssets', $asset); $this->requirePeerVolumePermissionByAsset('deletePeerAssets', $asset);
The fix that landed in 05c2042 for actionMoveFolder (src/controllers/AssetsController.php:733-765) added both savePeerAssets and deletePeerAssets requireVolumePermissionByFolder checks to mirror the per-asset pattern, but the same hardening was not applied to actionDeleteFolder or actionRenameFolder (which also calls deleteFoldersByIds indirectly through later logic).
The asymmetry between the two endpoints demonstrates the missing check.
Impact
- Integrity / availability of other users' assets on any volume where the attacker has deleteAssets but not deletePeerAssets: the attacker can permanently delete peer-owned files (and their parent folder structure) on the underlying filesystem, with no recovery via Craft's UI. - The Craft permission model explicitly distinguishes "delete your own assets" (deleteAssets) from "delete other users' assets" (deletePeerAssets) precisely so administrators can grant the former without the latter on shared volumes — this finding renders that distinction unenforceable for any user given folder-delete rights. - No information disclosure or remote code execution; impact is bounded to the affected volume's contents. - Does not require any non-default configuration: the affected endpoint is enabled by default and only requires that an administrator has split deleteAssets from deletePeerAssets (the documented, supported permission model).
Summary
There is a mass-assignment flaw in the bulk-duplicate element action. Alice, holding only the permission to duplicate an entry she owns, submits an arbitrary id through the newAttributes request parameter. The duplication routine overrides its own id = null reset with that value and writes Alice’s attributes into Bob’s existing entry row.
Details
ElementsController::beforeAction() (src/controllers/ElementsController.php:119-124) pulls the request body into $this->attributes and rejects requests that ship an id or canonicalId key at the top level:
php $this->attributes = $this->request->getBodyParams();
// No funny business if (isset($this->attributes['id']) || isset($this->attributes['canonicalId'])) { throw new BadRequestHttpException('Changing an element’s ID is not allowed.'); }
The check inspects only the top-level payload. actionBulkDuplicate() (src/controllers/ElementsController.php:1708-1749) reads a separate newAttributes array and passes it straight through to the service layer:
php $elementInfo = $this->request->getRequiredBodyParam('elements'); $newAttributes = $this->request->getRequiredBodyParam('newAttributes'); ... $safeNewAttributes = Collection::make($newAttributes) ->only($element->safeAttributes()) ->all(); ... $newElement = $elementsService->duplicateElement( $element, $safeNewAttributes + $element::baseBulkDuplicateAttributes(), false, checkAuthorization: true, );
Elements::duplicateElement() (src/services/Elements.php:1814-1840) clones the source element, sets id to null, and then hands the attacker's array to Craft::configure():
php $mainClone = clone $element; $mainClone->id = null; $mainClone->uid = StringHelper::UUID(); ... Craft::configure($mainClone, ArrayHelper::merge( $newAttributes, $siteAttributes[$mainClone->siteId] ?? [], ));
Craft::configure() overwrites the reset id with any numeric value inside $newAttributes. Yii's saveElement() then performs an UPDATE against the row with that primary key instead of an INSERT. Alice's title, slug, authorId, postDate, and UID land on Bob’s entry.
safeAttributes() on Entry includes id because the base element model exposes it, so the Collection::only() filter does not strip it.
Impact
A low-privileged author overwrites any other element (entries, categories, users that share the Entry element table inheritance) by predicting or enumerating element IDs. Content integrity on the entire install breaks. The attack requires only the ability to duplicate one entry Alice already owns.
Craft CMS versions >= 5.0.0-RC1 before 5.10.6 and >= 4.0.0-RC1 before 4.18.2 interpolate environment variables and secrets (via ${ENVVAR} strings in the elementId parameter) into Twig templates before rendering, even when the Twig sandbox is enabled. An authenticated attacker with control panel access can render a malicious sandboxed Twig template and, using a blind error-based technique across many requests, incrementally leak arbitrary environment variables and secrets. These can be abused to forge sessions (via CRAFTSECURITYKEY), escalate privileges, and steal database, SMTP, API, or blob storage credentials. Fixed in 5.10.6 and 4.18.2.
Craft CMS before 5.10.5 fails to persist updated credential counters after WebAuthn assertion validation in the passkey login endpoint. Attackers can replay captured login request bodies containing requestOptions and response to create additional authenticated sessions for victim accounts.
Craft CMS versions before 5.10.8 contain an authentication bypass vulnerability in the elements/save action that allows authenticated users to change passwords without verification. Attackers with edit users permission can reset any user's password including administrators by exploiting the unprotected newPassword field in the User element save flow.
Craft CMS versions before 5.10.11 lack authorization checks in the assets/move-asset endpoint when force=1 is supplied. Authenticated users without peer asset permissions can move their own assets into other users' folders and force deletion of conflicting files, allowing unauthorized asset deletion and replacement.
Craft CMS versions >= 5.0.0-RC1 and < 5.10.11 fail to perform an independent authorization check in ElementsController::actionDeleteForSite(). The method loads an element with checkForProvisionalDraft enabled and runs the deletion authorization check against the user's own provisional draft (which only verifies draft ownership), then propagates the deletion to the canonical element without re-checking permissions. As a result, an authenticated user who has viewEntries, viewPeerEntries, saveEntries, savePeerEntries, and editSite permissions but lacks the deleteEntriesForSite permission can hard-delete a canonical entry's site record (and, for single-site entries, the full element and content), which is irrecoverable via Craft's recycle bin.
Craft CMS versions >= 5.0.0-RC1 and < 5.10.11 contain a missing authorization vulnerability in AssetsController::actionReplaceFile. When a request supplies sourceAssetId and targetFilename but omits assetId, the target asset is resolved by folder and filename after the permission checks execute, so the replacePeerFiles permission is never enforced. An authenticated low-privilege author with only the replaceFiles permission on a shared folder can overwrite the content of a peer's asset file (located in the same folder) with attacker-controlled bytes. Fixed in 5.10.11.
Craft CMS GraphQL entry mutation resolvers (saveEntry, deleteEntry) read siteIddirectly from$argumentswithout passing throughArgumentManagerprepareArguments(), which is the function that enforces site-scope filtering via arrayintersect against the GraphQL schema’s allowed sites. The query path (ElementResolverprepareElementQuery) correctly calls prepareArguments(), so queries to unauthorized sites return empty. But mutations bypass this entirely — an attacker with a token scoped to Site A can create, modify, or delete entries in Site B by passing siteId in the mutations argument.
Craft CMS versions 5.0.0-RC1 through 5.10.11 are missing an admin-target guard in UsersController::actionActivateUser (the users/activate-user action). While the action requires the administrateUsers permission, it does not call requireAdmin() when the targeted user is an administrator, unlike the mirror action actionDeactivateUser. As a result, an authenticated control panel user who is not an administrator but holds the administrateUsers permission can activate a pending or deliberately deactivated administrator account, which can lead to permission escalation when combined with resetting that account's password. The issue is fixed in Craft CMS 5.10.12.
Craft CMS contains an external control of assumed-immutable web parameter vulnerability. This vulnerability could allow an unauthenticated client to introduce arbitrary values, such as PHP code, to a known local file location on the server. This vulnerability could be chained with CVE-2024-58136 as represented by CVE-2025-32432.