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`. ### Description Craft CMS’s `craft\\controllers\\AssetsController::actionReplaceFile()` supports replacing a target asset file using another existing asset as the source. The action loads: - `$assetToReplace` from `assetId` - `$sourceAsset` from `sourceAssetId` It 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. ```php $this->requireVolumePermissionByAsset('replaceFiles', $assetToReplace ?: $sourceAsset); $this->requirePeerVolumePermissionByAsset('replacePeerFiles', $assetToReplace ?: $sourceAsset); ``` [*src/controllers/AssetsController.php:L433-L434*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L433-L434) In 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. ```php $assets->replaceAssetFile($assetToReplace, $tempPath, $assetToReplace->getFilename(), $sourceAsset->getMimeType()); Craft::$app->getElements()->deleteElement($sourceAsset); ``` [*src/controllers/AssetsController.php:L462-L463*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L462-L463) ### Impact An 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.
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`. ### Description Craft CMS’s `craft\\controllers\\AssetsController::actionReplaceFile()` supports replacing a target asset file using another existing asset as the source. The action loads: - `$assetToReplace` from `assetId` - `$sourceAsset` from `sourceAssetId` It 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. ```php $this->requireVolumePermissionByAsset('replaceFiles', $assetToReplace ?: $sourceAsset); $this->requirePeerVolumePermissionByAsset('replacePeerFiles', $assetToReplace ?: $sourceAsset); ``` [*src/controllers/AssetsController.php:L433-L434*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L433-L434) In 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. ```php $assets->replaceAssetFile($assetToReplace, $tempPath, $assetToReplace->getFilename(), $sourceAsset->getMimeType()); Craft::$app->getElements()->deleteElement($sourceAsset); ``` [*src/controllers/AssetsController.php:L462-L463*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L462-L463) ### Impact An 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.
Update craftcms/cms to 5.9.21; craftcms/cms to 4.17.14 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanCraft CMS: Unauthorized Deletion of Source Assets During File Replacement affects craftcms/cms (composer), craftcms/cms (composer). Severity is medium. 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`. ### Description Craft CMS’s `craft\\controllers\\AssetsController::actionReplaceFile()` supports replacing a target asset file using another existing asset as the source. The action loads: - `$assetToReplace` from `assetId` - `$sourceAsset` from `sourceAssetId` It 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. ```php $this->requireVolumePermissionByAsset('replaceFiles', $assetToReplace ?: $sourceAsset); $this->requirePeerVolumePermissionByAsset('replacePeerFiles', $assetToReplace ?: $sourceAsset); ``` [*src/controllers/AssetsController.php:L433-L434*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L433-L434) In 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. ```php $assets->replaceAssetFile($assetToReplace, $tempPath, $assetToReplace->getFilename(), $sourceAsset->getMimeType()); Craft::$app->getElements()->deleteElement($sourceAsset); ``` [*src/controllers/AssetsController.php:L462-L463*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L462-L463) ### Impact An 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.
AI coding agents often install or upgrade packages automatically in composer. A medium vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| craftcms/cmscomposer | >=5.0.0-RC1,<5.9.21 | 5.9.21 |
| craftcms/cmscomposer | >=4.0.0-RC1,<4.17.14 | 4.17.14 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate craftcms/cms to 5.9.21; craftcms/cms to 4.17.14 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanCraft CMS: Unauthorized Deletion of Source Assets During File Replacement affects craftcms/cms (composer), craftcms/cms (composer). Severity is medium. 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`. ### Description Craft CMS’s `craft\\controllers\\AssetsController::actionReplaceFile()` supports replacing a target asset file using another existing asset as the source. The action loads: - `$assetToReplace` from `assetId` - `$sourceAsset` from `sourceAssetId` It 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. ```php $this->requireVolumePermissionByAsset('replaceFiles', $assetToReplace ?: $sourceAsset); $this->requirePeerVolumePermissionByAsset('replacePeerFiles', $assetToReplace ?: $sourceAsset); ``` [*src/controllers/AssetsController.php:L433-L434*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L433-L434) In 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. ```php $assets->replaceAssetFile($assetToReplace, $tempPath, $assetToReplace->getFilename(), $sourceAsset->getMimeType()); Craft::$app->getElements()->deleteElement($sourceAsset); ``` [*src/controllers/AssetsController.php:L462-L463*](https://github.com/craftcms/cms/blob/5.x/src/controllers/AssetsController.php#L462-L463) ### Impact An 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.
AI coding agents often install or upgrade packages automatically in composer. A medium vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| craftcms/cmscomposer | >=5.0.0-RC1,<5.9.21 | 5.9.21 |
| craftcms/cmscomposer | >=4.0.0-RC1,<4.17.14 | 4.17.14 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard