Face/Off: Hijacking Sensitive Assets via Profile Photos in Craft CMS
Jan 5, 2026·5 min read
Executive Summary (TL;DR)
By exploiting a Mass Assignment flaw in the user profile update logic, an attacker can manipulate the `photoId` parameter. This tricks Craft CMS into believing a restricted file (like a database backup) is the user's new avatar. The system then 'relocates' this sensitive asset into the public-facing profile directory, effectively granting the attacker full read access to files they should never see.
A critical Insecure Direct Object Reference (IDOR) vulnerability combined with Mass Assignment in Craft CMS allows authenticated users to misappropriate any system asset—including private backups and configuration files—by setting it as their profile photo.
The Hook: Your Face Looks Like a Database Backup
Craft CMS is a darling of the PHP world, known for its flexibility and sleek usage of the Yii2 framework. Like any good CMS, it allows users to have profiles, and those profiles have photos. It’s a simple feature: you upload a JPEG, the server stores it, and it links that image ID to your user account.
But what happens when the code trusts you a little too much? In a secure system, when you say "Image #500 is my photo," the system should ask two questions: "Is Image #500 actually an image?" and "Do you actually own Image #500?"
CVE-2025-68436 is the result of Craft CMS failing to ask those questions. It’s a classic tale of Mass Assignment meeting IDOR. The result is a vulnerability where I don't need to upload a photo to change my face. I can just tell the server that your private tax return PDF is my new face. And the server, being the helpful digital butler it is, doesn't just link it—it grabs that PDF from the secure vault and moves it right onto the front porch so everyone can see it.
The Flaw: Mass Assignment & The Lazy Loader
To understand this bug, you have to look at how the Yii2 framework handles data. Yii2 uses a concept called Mass Assignment via the $model->load($_POST) method. This allows developers to take a giant array of form data and dump it directly into a model's attributes in one shot. It is efficient, cleaner than writing fifty lines of $user->name = $_POST['name'], and dangerous as hell if you aren't careful.
To prevent users from overwriting sensitive fields (like isAdmin or passwordHash), models use a safeAttributes() method (or scenarios) to define an allowlist. If a field isn't safe, Mass Assignment ignores it.
In affected versions of Craft CMS, the photoId attribute was implicitly treated as 'safe' during profile updates. This meant that any POST request to users/save-user could include a photoId parameter. The application didn't check if the user had just uploaded that file. It simply accepted the ID provided in the request body.
But the flaw goes deeper. The logic responsible for handling user photos involves asset relocation. If a user sets a new photo, Craft CMS often attempts to move that asset into a specific 'User Photos' volume. So, the attacker isn't just referencing a file; they are triggering a server-side operation that physically moves a file from a potentially restricted storage volume into a public-facing directory.
The Code: Autopsy of a Safe Attribute
The fix was applied in src/elements/User.php and src/controllers/UsersController.php. The core issue was that photoId was allowed to drift in via the request parameters without explicit filtering.
Here is the critical change in the User model. The developers had to explicitly tell the model not to trust the photoId coming from the wild:
// src/elements/User.php
public function safeAttributes(): array
{
// BEFORE: photoId was implicitly included or not excluded
// AFTER: Explicitly remove 'photoId' from safe attributes
return ArrayHelper::withoutValue(parent::safeAttributes(), 'photoId');
}Additionally, the UsersController was patched to validate ownership. Before the fix, the controller blindly passed the photoId to the service layer. The patch forces a check to ensure the asset is either already the user's photo or a temporary upload owned by the current user:
// src/controllers/UsersController.php
// Pseudo-code of the added validation logic
if ($photoId) {
$asset = Asset::find()->id($photoId)->one();
if (!$asset || !$this->canViewAsset($asset)) {
throw new ForbiddenHttpException('Nice try, hackerman.');
}
}This double-tap fix—hardening the model against mass assignment and hardening the controller logic—effectively closes the hole.
The Exploit: Stealing Secrets with a POST Request
Let's weaponize this. Imagine we are an authenticated user with low privileges (a standard member account). We want to access a sensitive file we know exists in the system—perhaps a database backup stored in a restricted volume with Asset ID 1337.
We don't need fancy tools; curl or Burp Suite will do.
Step 1: The Setup First, we log in normally to get our session cookie.
Step 2: The Attack
We intercept the 'Save Profile' request and modify it. We inject the photoId parameter pointing to our target asset.
POST /index.php?p=admin/actions/users/save-user HTTP/1.1
Host: target-craft-cms.com
Cookie: CRAFT_CSRF_TOKEN=...; CraftSessionId=...
Content-Type: application/x-www-form-urlencoded
action=users/save-user
&userId=CURRENT_USER_ID
&photoId=1337 <-- The ID of the sensitive fileStep 3: The Relocation
Craft CMS receives this, sees photoId=1337, and thinks, "Oh, the user selected this file as their avatar." The internal logic kicks in: "Profile photos belong in the /images/avatars/ folder." It takes Asset 1337 (our database backup), moves it to the public folder, and updates the database.
Step 4: The Loot
We simply refresh our profile page or look at the source code for our avatar URL:
https://target-craft-cms.com/images/avatars/user_123/db_backup.sql
We download the file, parse the SQL, and dump the users table. Game over.
The Impact: Why This Matters
This vulnerability is a perfect example of how business logic flaws can outweigh complex memory corruptions in terms of real-world impact. We aren't overflowing buffers here; we are abusing the system's own organizational rules.
Confidentiality: High. Attackers can read any asset known to the system, provided they can guess the ID (which is often sequential integers). This includes contracts, invoices, backups, and config files stored as assets.
Integrity: High. The exploit involves moving the file. This alters the state of the file system. A critical document in a 'Secure' folder is now physically located in a 'Public' folder. Even if the vulnerability is patched later, the file might remain in the wrong location.
Availability: Low to Medium. By moving critical assets out of their expected paths, an attacker could potentially break other parts of the site that rely on those files being in specific volumes.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | < 4.16.17 | 4.16.17 |
Craft CMS Pixel & Tonic | < 5.8.21 | 5.8.21 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-915 (Mass Assignment) |
| Secondary CWE | CWE-639 (Insecure Direct Object Reference) |
| Attack Vector | Network (HTTP POST) |
| CVSS v3.1 | 7.5 (High) |
| Exploit Status | Functional PoC Available |
| Privileges Required | Low (Authenticated User) |
MITRE ATT&CK Mapping
The application allows a client to supply multiple attributes in an input, but fails to verify that the attributes are valid for assignment, allowing attackers to modify sensitive data fields.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.