BraveCMS's CKEditor upload handler accepts arbitrary file types without validation. The ckupload method in CkEditorController.php relies entirely on user-supplied input to determine the file type, making it trivial for an authenticated user to upload a PHP script disguised as an image. Once uploaded, the file is stored in a publicly accessible directory and can be requested directly to achieve remote code execution on the server.
Details
Summary
| Field |
Value |
| CVE ID |
CVE-2026-35164 |
| Advisory |
GHSA-2j4q-6p52-4rhw |
| Severity |
High |
| CWE |
CWE-434 (Unrestricted Upload of File with Dangerous Type) |
| Package |
BraveCMS (PHP) |
| Affected |
2.0.0 |
| Patched |
2.0.6 |
| Vulnerability |
Unrestricted File Upload leading to RCE |
| Impact |
Remote Code Execution as web server user |
The ckupload method in app/Http/Controllers/Dashboard/CkEditorController.php processes file uploads from the CKEditor rich text editor. It performs no validation of the uploaded file's actual type and makes no attempt to verify that the file content matches an expected image format. Because the upload destination is a publicly served directory under /images/articles/, any file placed there is directly accessible over HTTP. An attacker with Author-level privileges can upload a PHP webshell and execute arbitrary commands on the server by requesting the uploaded file.
How it works
The ckupload method stores the uploaded file using a name derived from an MD5 hash, but applies no extension filtering or MIME type validation:
// app/Http/Controllers/Dashboard/CkEditorController.php
public function ckupload(Request $request)
{
// No validation: file type is taken entirely from user-supplied input
$file = $request->file('upload');
$filename = md5($file->getClientOriginalName()) . $file->getClientOriginalExtension();
$file->move(public_path('images/articles'), $filename);
return response()->json([
'url' => asset('images/articles/' . $filename)
]);
}
The critical failure is the use of getClientOriginalExtension(), which reads the extension from the filename the client sent in the multipart Content-Disposition header. This value is fully attacker-controlled and is never cross-checked against the file's actual content or MIME type. The uploaded file lands in public/images/articles/, which the web server serves as static content, including .php files that PHP-FPM or mod_php will execute on request.
Root Cause
The upload handler trusts the client-supplied filename extension to determine what kind of file is being uploaded. No server-side validation of file content, MIME type, or extension against an allowlist is performed at any point. Storing executable files in a web-accessible directory without restricting script execution makes the impact immediate: upload and execute are a single step apart.
Prerequisites
- A valid BraveCMS account with at least Author privileges.
- Access to the dashboard and the CKEditor-enabled compose or edit interface.
- No additional configuration or environmental condition is required.
Fix
Version 2.0.6 addresses this by enforcing strict server-side validation in the upload handler. The recommended implementation uses Laravel's built-in validation rules to restrict uploads to known-safe image types and replaces the client-supplied extension with one derived from the validated file itself:
// After (safe): server-side validation, no trust in client-supplied extension
public function ckupload(Request $request)
{
$request->validate([
'upload' => 'required|image|mimes:jpeg,jpg,png,gif|max:2048'
]);
$file = $request->file('upload');
// Use server-detected extension, not the client-supplied one
$filename = md5(uniqid()) . '.' . $file->extension();
$file->move(public_path('images/articles'), $filename);
return response()->json([
'url' => asset('images/articles/' . $filename)
]);
}
As an additional layer of defence, the web server should be configured to deny PHP execution in the upload directory entirely, so that even if a bypass is discovered later, uploaded files cannot be executed:
# Nginx: deny script execution in the upload directory
location ~* ^/images/articles/.*\.php$ {
deny all;
}
# Apache: deny script execution in the upload directory
<Directory "/var/www/html/public/images/articles">
php_flag engine off
Options -ExecCGI
RemoveHandler .php .php5 .phtml
</Directory>
Key Takeaways
- Never trust client-supplied file metadata.
getClientOriginalExtension() and getClientOriginalName() return values the attacker controls entirely. File type must always be determined server-side from the file content.
- Upload directories must never execute scripts. Regardless of how thorough the upload validation is, the web server should be configured to treat the upload directory as static-only. This provides defence in depth if a bypass is found later.
- Author-level access is not low risk. In CMS platforms, content editor roles often have access to upload endpoints. Treating these endpoints as low-privilege is a common mistake; they are a direct path to RCE if validation is absent.
- Upgrade to 2.0.6 immediately. The patched version enforces both server-side MIME validation and safe extension derivation, eliminating the attack surface.