Skip to content

Commit d072147

Browse files
1 parent ce2637f commit d072147

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-43cq-c2gq-pfpw",
4+
"modified": "2026-07-02T18:47:37Z",
5+
"published": "2026-07-02T18:47:37Z",
6+
"aliases": [
7+
"CVE-2026-50280"
8+
],
9+
"summary": "Craft CMS: Authorization bypass in `entries/move-to-section` via missing target-section save check",
10+
"details": "### Summary\n\nThe `EntriesController::actionMoveToSection()` endpoint checks only whether the current user can view the destination section, but it does not require permission to save entries into that section. A low-privileged authenticated control-panel user who can move an entry out of its current section can therefore move that entry into a different section where they have read access but no write access.\n\n### Details\n\nThe vulnerable route is implemented in [EntriesController.php](/D:/files/projects/cms-5.9.19/cms-5.9.19/src/controllers/EntriesController.php):465:\n\nThe destination check is only `viewEntries:$section->uid` . The source-entry gate is `Entry::canMove()`, which verifies whether the user can move the existing entry based on the source section:\n\nThis closes the exploit chain:\n\n1. External source: authenticated CP request to `entries/move-to-section`.\n2. Missing authorization check: destination section requires only `viewEntries`, not `saveEntries`.\n3. Privileged sink: `moveEntryToSection()` rewrites `sectionId` and saves the entry into the unauthorized section.\n\nPreconditions derived from the code:\n\n1. The attacker is authenticated to the control panel.\n2. Entry `345` is movable by the attacker from its current section.\n3. The attacker can satisfy `viewEntries` on destination section `12`.\n4. The attacker does not have `saveEntries:DESTINATION_UID`, which is the missing check that makes the bypass possible.\n\nResult:\n\n1. The controller accepts the request because `viewEntries:$section->uid` passes.\n2. Each source entry passes `canMove()` based on source-section permissions.\n3. `moveEntryToSection()` updates the entry’s `sectionId` and saves it.\n4. The entry is now located in a section where the attacker did not have write permission.\n\n### Impact\n\nThis breaks the intended section-level authorization model. A user with limited content permissions can inject or relocate content into a protected section, interfering with editorial boundaries, approval workflows, section-specific business logic, and content ownership expectations.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "craftcms/cms"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "5.0.0-RC1"
29+
},
30+
{
31+
"fixed": "5.9.21"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-43cq-c2gq-pfpw"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-50280"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/craftcms/cms/commit/0a6b916f6367b0162b2eaf2366add67b45fa98ea"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/craftcms/cms"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-284"
59+
],
60+
"severity": "MODERATE",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-07-02T18:47:37Z",
63+
"nvd_published_at": "2026-07-02T00:16:44Z"
64+
}
65+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-7h62-6v23-v8fm",
4+
"modified": "2026-07-02T18:49:04Z",
5+
"published": "2026-07-02T18:49:04Z",
6+
"aliases": [
7+
"CVE-2026-50284"
8+
],
9+
"summary": "Craft CMS: Missing peer-permission check in `AssetsController::actionDeleteFolder` allows deletion of other users' assets",
10+
"details": "## Summary\n\n`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.\n\nThis 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.\n\n## Details\n\n`src/controllers/AssetsController.php:552-569`:\n\n```php\npublic function actionDeleteFolder(): Response\n{\n $this->requireAcceptsJson();\n $folderId = $this->request->getRequiredBodyParam('folderId');\n\n $assets = Craft::$app->getAssets();\n $folder = $assets->getFolderById($folderId);\n\n if (!$folder) {\n throw new BadRequestHttpException('The folder cannot be found');\n }\n\n // Check if it's possible to delete objects in the target volume.\n $this->requireVolumePermissionByFolder('deleteAssets', $folder); // <-- only checks deleteAssets\n $assets->deleteFoldersByIds($folderId);\n\n return $this->asSuccess();\n}\n```\n\n`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.\n\n`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:\n\n```php\n$assetQuery = Asset::find()->folderId($allFolderIds);\n$elementService = Craft::$app->getElements();\n\nforeach (Db::each($assetQuery) as $asset) {\n $asset->keepFileOnDelete = !$deleteDir;\n $elementService->deleteElement($asset, true);\n}\n```\n\nThis bypasses `Asset::canDelete()` (`src/elements/Asset.php:1515-1536`):\n\n```php\npublic function canDelete(User $user): bool\n{\n if ($this->isFolder) { return false; }\n if (parent::canDelete($user)) { return true; }\n $volume = $this->getVolume();\n if (Assets::isTempUploadFs($volume->getFs())) { return true; }\n\n if ($this->uploaderId !== $user->id) {\n return $user->can(\"deletePeerAssets:$volume->uid\"); // <-- never reached on cascade delete\n }\n return $user->can(\"deleteAssets:$volume->uid\");\n}\n```\n\nCompare to `actionDeleteAsset` (`src/controllers/AssetsController.php:579-613`), which correctly does:\n\n```php\n$this->requireVolumePermissionByAsset('deleteAssets', $asset);\n$this->requirePeerVolumePermissionByAsset('deletePeerAssets', $asset);\n```\n\nThe 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).\n\nThe asymmetry between the two endpoints demonstrates the missing check.\n\n## Impact\n\n- 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.\n- 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.\n- No information disclosure or remote code execution; impact is bounded to the affected volume's contents.\n- 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).",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:L/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "craftcms/cms"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "5.0.0-RC1"
29+
},
30+
{
31+
"fixed": "5.9.22"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Packagist",
40+
"name": "craftcms/cms"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "4.0.0-RC1"
48+
},
49+
{
50+
"fixed": "4.17.15"
51+
}
52+
]
53+
}
54+
]
55+
}
56+
],
57+
"references": [
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-7h62-6v23-v8fm"
61+
},
62+
{
63+
"type": "ADVISORY",
64+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-50284"
65+
},
66+
{
67+
"type": "WEB",
68+
"url": "https://github.com/craftcms/cms/commit/b4e08977f0c9bdf002a77f9f6d1346cd55ac0598"
69+
},
70+
{
71+
"type": "PACKAGE",
72+
"url": "https://github.com/craftcms/cms"
73+
}
74+
],
75+
"database_specific": {
76+
"cwe_ids": [
77+
"CWE-862"
78+
],
79+
"severity": "HIGH",
80+
"github_reviewed": true,
81+
"github_reviewed_at": "2026-07-02T18:49:04Z",
82+
"nvd_published_at": "2026-07-01T23:16:52Z"
83+
}
84+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-qh45-9g5p-m2v4",
4+
"modified": "2026-07-02T18:48:27Z",
5+
"published": "2026-07-02T18:48:27Z",
6+
"aliases": [
7+
"CVE-2026-50283"
8+
],
9+
"summary": "Craft CMS: Unauthorized Deletion of Source Assets During File Replacement",
10+
"details": "We have identified an authorization issue in Craft CMS `AssetsController::actionReplaceFile` that can delete a source asset without source delete permission by supplying both `assetId` and `sourceAssetId`.\n\n### Description\n\nCraft CMS’s `craft\\\\controllers\\\\AssetsController::actionReplaceFile()` supports replacing a target asset file using another existing asset as the source. The action loads:\n\n- `$assetToReplace` from `assetId` \n- `$sourceAsset` from `sourceAssetId`\n\nIt then enforces replace permissions using `($assetToReplace ?: $sourceAsset)`. When both IDs are provided, this expression resolves to the target asset so no permission check is performed against the source asset volume.\n\n```php\n$this->requireVolumePermissionByAsset('replaceFiles', $assetToReplace ?: $sourceAsset);\n$this->requirePeerVolumePermissionByAsset('replacePeerFiles', $assetToReplace ?: $sourceAsset);\n```\n\n[*src/controllers/AssetsController.php:L433-L434*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L433-L434)\n\nIn the branch where both assets are present, Craft copies the source file into the target and then deletes the source asset. There is no check for `deleteAssets:<sourceVolumeUid>` or `deletePeerAssets:<sourceVolumeUid>` for the source asset before deletion.\n\n```php\n$assets->replaceAssetFile($assetToReplace, $tempPath, $assetToReplace->getFilename(), $sourceAsset->getMimeType());\nCraft::$app->getElements()->deleteElement($sourceAsset);\n```\n\n[*src/controllers/AssetsController.php:L462-L463*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L462-L463)\n\n### Impact\n\nAn authenticated user who can replace files in one volume can delete assets in another volume where they do not have delete permission, as long as they can obtain a `sourceAssetId`. This can lead to unauthorized asset deletion, broken content references, and data loss.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "craftcms/cms"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "5.0.0-RC1"
29+
},
30+
{
31+
"fixed": "5.9.21"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Packagist",
40+
"name": "craftcms/cms"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "4.0.0-RC1"
48+
},
49+
{
50+
"fixed": "4.17.14"
51+
}
52+
]
53+
}
54+
]
55+
}
56+
],
57+
"references": [
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-qh45-9g5p-m2v4"
61+
},
62+
{
63+
"type": "ADVISORY",
64+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-50283"
65+
},
66+
{
67+
"type": "WEB",
68+
"url": "https://github.com/craftcms/cms/commit/2c2579c7f1030872423f268d0c8b48377101961d"
69+
},
70+
{
71+
"type": "PACKAGE",
72+
"url": "https://github.com/craftcms/cms"
73+
}
74+
],
75+
"database_specific": {
76+
"cwe_ids": [
77+
"CWE-639"
78+
],
79+
"severity": "MODERATE",
80+
"github_reviewed": true,
81+
"github_reviewed_at": "2026-07-02T18:48:27Z",
82+
"nvd_published_at": "2026-07-01T23:16:52Z"
83+
}
84+
}

0 commit comments

Comments
 (0)