Feb 9, 2026·5 min read·17 visits
Classic IDOR/Logic flaw. The system checks if you have permission for Volume A, but lets you modify an Asset from Volume B. If you can upload files anywhere, you can mess with files everywhere. Fixed in 5.8.22.
A high-severity privilege escalation vulnerability in Craft CMS allows authenticated users with write access to any asset volume to manipulate assets in volumes they shouldn't be able to touch. By exploiting a logic flaw in the GraphQL `saveAsset` mutation, attackers can bypass authorization checks and modify, move, or delete restricted files.
Access control is hard. It's even harder when you're juggling graph-based data structures where relationships aren't always explicitly enforced at the gate. CVE-2026-25497 is a perfect example of what I like to call the "Bouncer Blindspot."
Imagine walking into a high-security office building. The guard at the front desk checks your badge. "Oh, you work in the mailroom? Go right ahead." You walk past the mailroom and stroll right into the CEO's office. The guard checked who you were and where you said you were going, but nobody followed you to make sure you actually went there.
In technical terms, this is a logic flaw in Craft CMS's GraphQL API. Specifically, the saveAsset mutation. It suffers from a classic dissociation between the authorization object and the target object. It's a high-severity issue (CVSS 8.6) because it turns low-privileged users—content editors, interns, or compromised accounts—into asset administrators for the entire system.
The vulnerability lives in the gap between Verification and Action. The saveAsset mutation in Craft's GraphQL API takes two critical arguments: a volume ID and an asset ID.
The logic flow went something like this:
Spot the problem? The code verified rights on the Volume passed in the arguments, but it performed the operation on the Asset passed in the arguments. It never stopped to ask, "Wait a minute, does Asset 500 actually belong to Volume 1?"
If Asset 500 is actually in Volume 99 (Restricted HR Documents), the system didn't care. It was too busy looking at the permissions for Volume 1. This is a textbook CWE-639: Authorization Bypass Through User-Controlled Key.
Let's look at the PHP code responsible for this mess. This resides in src/gql/resolvers/mutations/Asset.php.
The Vulnerable Code:
// It checks permissions on the provided volume ($volume)
$this->requireSchemaAction('volumes.' . $volume->uid, 'save');
// Then it fetches the asset by ID, ignoring the volume context
$asset = Asset::find()->id($arguments['id'])->one();
if (!$asset) {
throw new Error('No such asset exists');
}
// Proceeds to save $asset...The developer trusted the client to provide a matching pair of Volume ID and Asset ID. Never trust the client. The client is a liar.
The Fix (Commit ac7edf8):
The patch adds a sanity check. If the Asset's actual Volume ID doesn't match the Volume ID passed in the request, it forces a new permission check against the real volume.
// ... asset fetched ...
if ($asset->volumeId !== $volume->id) {
// Oh, you're trying to reach across volumes?
// Let's check if you have rights for the REAL volume.
$this->requireSchemaAction('volumes.' . $asset->getVolume()->uid, 'save');
}This simple if statement closes the gap. It forces the authorization context to switch to the asset's actual location, effectively slamming the door on the exploit.
Exploiting this is trivially easy if you have a valid account with write access to any volume. Let's say you are a low-level editor allowed to upload images to the "Blog Images" volume.
Step 1: Reconnaissance
You need two things: the ID of a volume you own (e.g., volumeId: 10) and the ID of an asset you want to mess with (e.g., assetId: 1337). You can often enumerate asset IDs just by incrementing numbers or looking at the site's predictable usage of GraphQL queries.
Step 2: The Attack You construct a GraphQL mutation. You tell the system you are operating in the context of your allowed volume, but you target the restricted asset.
mutation {
saveAsset(
volumeId: 10, # Your allowed volume
id: 1337, # The CEO's private tax return (actually in Volume 1)
title: "HACKED_BY_INTERN"
) {
id
title
}
}Step 3: The Impact The server checks if you can write to Volume 10. You can. It then updates Asset 1337 with your new title.
Even worse, saveAsset can handle file moves. You could potentially change the newLocation of the asset, effectively moving a private document into your public "Blog Images" folder, allowing you to download it. This escalates the issue from "Defacement" to "Data Exfiltration."
There is no fancy workaround here. You need to patch. The logic flaw is deep in the resolver code.
Upgrade Paths:
If you absolutely cannot patch immediately (why?), your only mitigation is to strip write permissions from everyone untrusted. If a user has no write access to any volume, they cannot pass the initial check, and the exploit chain fails. But seriously, just update the CMS.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.0.0-RC1, < 4.17.0-beta.1 | 4.17.0-beta.1 |
Craft CMS Pixel & Tonic | >= 5.0.0-RC1, < 5.8.22 | 5.8.22 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-639 |
| Attack Vector | Network (GraphQL) |
| CVSS Score | 8.6 (High) |
| Impact | Privilege Escalation / Data Manipulation |
| Prerequisites | Authenticated user with write access to at least one volume |
| KEV Status | Not Listed |
The application authorizes a user based on a provided key (Volume ID) but performs the action on a different object (Asset ID) without verifying the relationship between the two.