VYPR
Moderate severityNVD Advisory· Published Aug 9, 2025· Updated Aug 11, 2025

Craft contains a theoretical bypass for CVE-2025-23209

CVE-2025-54417

Description

Craft is a platform for creating digital experiences. Versions 4.13.8 through 4.16.2 and 5.5.8 through 5.8.3 contain a vulnerability that can bypass CVE-2025-23209: "Craft CMS has a potential RCE with a compromised security key". To exploit this vulnerability, the project must meet these requirements: have a compromised security key and create an arbitrary file in Craft's /storage/backups folder. With those criteria in place, attackers could create a specific, malicious request to the /updater/restore-db endpoint and execute CLI commands remotely. This issue is fixed in versions 4.16.3 and 5.8.4.

AI Insight

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

Craft CMS versions 4.13.8-4.16.2 and 5.5.8-5.8.3 allow remote code execution via a bypass of CVE-2025-23209 when the security key is compromised and a file can be placed in the backups folder.

This vulnerability is a bypass of CVE-2025-23209, which itself allowed remote code execution in Craft CMS with a compromised security key [1]. The bypass affects versions 4.13.8 through 4.16.2 and 5.5.8 through 5.8.3 [2]. To exploit it, an attacker must have compromised the application's security key and be able to create an arbitrary file in the /storage/backups folder [2].

Exploitation involves sending a crafted malicious request to the /updater/restore-db endpoint, which previously could restore database backups [3]. Under the specified conditions, this endpoint can be abused to execute arbitrary CLI commands remotely [2]. The vulnerability does not require authentication if the security key is already known, but the prerequisite of a compromised key limits the attack surface to cases where the key has been leaked or guessed.

Successful exploitation grants an attacker remote code execution on the server, potentially leading to full compromise of the Craft CMS installation and associated data [2]. This is a critical vulnerability because it bypasses the fix for CVE-2025-23209, which was intended to prevent such attacks [1].

The issue is fixed in Craft CMS versions 4.16.3 and 5.8.4 [2]. The fix involved deprecating and removing the database restore functionality from the web updater endpoint, as shown in commit a19d46b [4]. Users running affected versions should update immediately; if unable to update, rotating the security key and ensuring its secrecy can reduce risk, but does not fully mitigate the bypass [1].

AI Insight generated on May 19, 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
>= 4.13.8, < 4.16.34.16.3
craftcms/cmsPackagist
>= 5.5.8, < 5.8.45.8.4

Affected products

2

Patches

1
a19d46be78a9

Drop automated DB restoring from web updates

https://github.com/craftcms/cmsbrandonkellyJul 7, 2025via ghsa
4 files changed · +16 51
  • CHANGELOG.md+4 0 modified
    @@ -1,5 +1,9 @@
     # Release Notes for Craft CMS 4
     
    +## Unreleased
    +
    +- Fixed a potential remote execution vulnerability.
    +
     ## 4.16.2 - 2025-07-04
     
     - Fixed a potential user account enumeration bug when `preventUserEnumeration` was enabled.
    
  • src/controllers/BaseUpdaterController.php+6 17 modified
    @@ -484,10 +484,9 @@ protected function finishedState(array $state = []): array
          * Runs the migrations for a given list of handles.
          *
          * @param string[] $handles
    -     * @param string|null $restoreAction
          * @return Response|null
          */
    -    protected function runMigrations(array $handles, ?string $restoreAction = null): ?Response
    +    protected function runMigrations(array $handles): ?Response
         {
             try {
                 Craft::$app->getUpdates()->runMigrations($handles);
    @@ -512,21 +511,11 @@ protected function runMigrations(array $handles, ?string $restoreAction = null):
                 Craft::error($error, __METHOD__);
                 Craft::$app->getErrorHandler()->logException($e);
     
    -            $options = [];
    -
    -            // Do we have a database backup to restore?
    -            if ($restoreAction !== null && !empty($this->data['dbBackupPath'])) {
    -                if (!empty($this->data['install'])) {
    -                    $restoreLabel = Craft::t('app', 'Revert update');
    -                } else {
    -                    $restoreLabel = Craft::t('app', 'Restore database');
    -                }
    -                $options[] = $this->actionOption($restoreLabel, $restoreAction);
    -            }
    -
    -            $options[] = [
    -                'label' => Craft::t('app', 'Troubleshoot'),
    -                'url' => 'https://craftcms.com/knowledge-base/failed-updates',
    +            $options = [
    +                [
    +                    'label' => Craft::t('app', 'Troubleshoot'),
    +                    'url' => 'https://craftcms.com/knowledge-base/failed-updates',
    +                ],
                 ];
     
                 if ($ownerHandle !== 'craft' && ($plugin = Craft::$app->getPlugins()->getPlugin($ownerHandle)) !== null) {
    
  • src/controllers/UpdaterController.php+6 30 modified
    @@ -12,9 +12,9 @@
     use Composer\Semver\VersionParser;
     use Craft;
     use craft\errors\InvalidPluginException;
    -use craft\helpers\FileHelper;
     use RequirementsChecker;
     use Throwable;
    +use yii\base\NotSupportedException;
     use yii\web\BadRequestHttpException;
     use yii\web\Response;
     
    @@ -31,6 +31,7 @@ class UpdaterController extends BaseUpdaterController
         public const ACTION_BACKUP = 'backup';
         public const ACTION_SERVER_CHECK = 'server-check';
         public const ACTION_REVERT = 'revert';
    +    /** @deprecated in 4.16.3 */
         public const ACTION_RESTORE_DB = 'restore-db';
         public const ACTION_MIGRATE = 'migrate';
     
    @@ -69,7 +70,7 @@ public function actionForceUpdate(): Response
         public function actionBackup(): Response
         {
             try {
    -            $this->data['dbBackupPath'] = Craft::$app->getDb()->backup();
    +            Craft::$app->getDb()->backup();
             } catch (Throwable $e) {
                 Craft::error('Error backing up the database: ' . $e->getMessage(), __METHOD__);
                 if (!empty($this->data['install'])) {
    @@ -97,35 +98,11 @@ public function actionBackup(): Response
          * Restores the database.
          *
          * @return Response
    +     * @deprecated in 4.16.3
          */
         public function actionRestoreDb(): Response
         {
    -        $backupPath = $this->data['dbBackupPath'];
    -        if (!file_exists($backupPath) || !FileHelper::isWithin($backupPath, Craft::$app->getPath()->getDbBackupPath())) {
    -            throw new BadRequestHttpException("Invalid backup path: $backupPath");
    -        }
    -
    -        try {
    -            Craft::$app->getDb()->restore($backupPath);
    -        } catch (Throwable $e) {
    -            Craft::error('Error restoring up the database: ' . $e->getMessage(), __METHOD__);
    -            return $this->send([
    -                'error' => Craft::t('app', 'Couldn’t restore the database. How would you like to proceed?'),
    -                'options' => [
    -                    $this->actionOption(Craft::t('app', 'Try again'), self::ACTION_RESTORE_DB),
    -                    $this->actionOption(Craft::t('app', 'Continue anyway'), self::ACTION_MIGRATE),
    -                ],
    -            ]);
    -        }
    -
    -        // Did we install new versions of things?
    -        if (!empty($this->data['install'])) {
    -            return $this->sendNextAction(self::ACTION_REVERT);
    -        }
    -
    -        return $this->sendFinished([
    -            'status' => Craft::t('app', 'The database was restored successfully.'),
    -        ]);
    +        throw new NotSupportedException('Restoring the database is no longer supported.');
         }
     
         /**
    @@ -206,7 +183,7 @@ public function actionMigrate(): Response
                 $handles = array_merge($this->data['migrate']);
             }
     
    -        return $this->runMigrations($handles, self::ACTION_RESTORE_DB) ?? $this->sendFinished();
    +        return $this->runMigrations($handles) ?? $this->sendFinished();
         }
     
         /**
    @@ -343,7 +320,6 @@ protected function actionStatus(string $action): string
             return match ($action) {
                 self::ACTION_FORCE_UPDATE => Craft::t('app', 'Updating…'),
                 self::ACTION_BACKUP => Craft::t('app', 'Backing-up database…'),
    -            self::ACTION_RESTORE_DB => Craft::t('app', 'Restoring database…'),
                 self::ACTION_MIGRATE => Craft::t('app', 'Updating database…'),
                 self::ACTION_REVERT => Craft::t('app', 'Reverting update (this may take a minute)…'),
                 self::ACTION_SERVER_CHECK => Craft::t('app', 'Checking server requirements…'),
    
  • src/translations/en/app.php+0 4 modified
    @@ -366,7 +366,6 @@
         'Couldn’t load CMS editions.' => 'Couldn’t load CMS editions.',
         'Couldn’t load active trials.' => 'Couldn’t load active trials.',
         'Couldn’t reorder items.' => 'Couldn’t reorder items.',
    -    'Couldn’t restore the database. How would you like to proceed?' => 'Couldn’t restore the database. How would you like to proceed?',
         'Couldn’t save address fields.' => 'Couldn’t save address fields.',
         'Couldn’t save email settings.' => 'Couldn’t save email settings.',
         'Couldn’t save entry type.' => 'Couldn’t save entry type.',
    @@ -1229,9 +1228,7 @@
         'Response:' => 'Response:',
         'Restart job' => 'Restart job',
         'Restart' => 'Restart',
    -    'Restore database' => 'Restore database',
         'Restore' => 'Restore',
    -    'Restoring database…' => 'Restoring database…',
         'Restrict allowed file types' => 'Restrict allowed file types',
         'Restrict assets to a single location' => 'Restrict assets to a single location',
         'Retry Duration' => 'Retry Duration',
    @@ -1474,7 +1471,6 @@
         'The base URL to the files in this filesystem.' => 'The base URL to the files in this filesystem.',
         'The base folder path that should be used as the root of the filesystem.' => 'The base folder path that should be used as the root of the filesystem.',
         'The command to run Sendmail with.' => 'The command to run Sendmail with.',
    -    'The database was restored successfully.' => 'The database was restored successfully.',
         'The developer recommends using <a href="{url}">{name}</a> instead.' => 'The developer recommends using <a href="{url}">{name}</a> instead.',
         'The draft could not be saved.' => 'The draft could not be saved.',
         'The draft has been saved.' => 'The draft has been saved.',
    

Vulnerability mechanics

Generated 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.