CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-25497
8.6

Craft CMS: The Old GraphQL Switcheroo

Alon Barad
Alon Barad
Software Engineer

Feb 9, 2026·5 min read·17 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: "I Have a Key, Does It Matter Which Door?"

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 Flaw: A Tale of Two Arguments

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:

  1. The user says, "I want to save this Asset (ID: 500) into this Volume (ID: 1)."
  2. The system asks, "Does this user have permission to write to Volume 1?"
  3. The permissions system says, "Yes, Volume 1 is the Public Uploads folder. They have access."
  4. The system then fetches Asset 500.
  5. The system saves the changes to Asset 500.

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.

The Code: The Smoking Gun

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.

The Exploit: Moving the Furniture

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

The Fix: Patch or Perish

There is no fancy workaround here. You need to patch. The logic flaw is deep in the resolver code.

Upgrade Paths:

  • Craft CMS 4: Update to 4.17.0-beta.1 or later.
  • Craft CMS 5: Update to 5.8.22 or 5.9.0-beta.1 or later.

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.

Official Patches

Craft CMSGitHub Commit fixing the issue
Craft CMSRelease notes for version 5.8.22

Fix Analysis (1)

Technical Appendix

CVSS Score
8.6/ 10
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

Affected Systems

Craft CMS 4.x (prior to 4.17.0-beta.1)Craft CMS 5.x (prior to 5.8.22)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Craft CMS
Pixel & Tonic
>= 4.0.0-RC1, < 4.17.0-beta.14.17.0-beta.1
Craft CMS
Pixel & Tonic
>= 5.0.0-RC1, < 5.8.225.8.22
AttributeDetail
CWE IDCWE-639
Attack VectorNetwork (GraphQL)
CVSS Score8.6 (High)
ImpactPrivilege Escalation / Data Manipulation
PrerequisitesAuthenticated user with write access to at least one volume
KEV StatusNot Listed

MITRE ATT&CK Mapping

T1222File and Directory Permissions Modification
Defense Evasion
T1078Valid Accounts
Initial Access
T1565Data Manipulation
Impact
CWE-639
Authorization Bypass Through User-Controlled Key

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.

Known Exploits & Detection

HypotheticalExploitation requires crafting a GraphQL mutation with mismatched volumeId and assetId arguments.

Vulnerability Timeline

Fix commit pushed to repository
2026-01-09
CVE Published
2026-02-09
GHSA Advisory Released
2026-02-09

References & Sources

  • [1]GHSA-fxp3-g6gw-4r4v
  • [2]NVD CVE-2026-25497

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.