Overview
| Item | Detail |
|---|---|
| CVE ID | CVE-2026-15748 |
| Affected product | Forminator Forms – Contact Form, Payment Form & Custom Form Builder (WordPress plugin) |
| Affected versions | 1.56.1 and earlier |
| Fixed in | 1.56.2 |
| CVSS 3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Type | CWE-434 (Unrestricted Upload of File with Dangerous Type) |
| Authentication required | None |
| Reported by | daroo, via the Wordfence Bug Bounty Program |
| Install base | Installed on more than 600,000 WordPress sites |
| Precondition | The targeted form must contain both a File Upload field and a Select field |
Forminator Forms is a form builder plugin for WordPress that handles contact forms, payment forms, and custom forms. Any form accepting file attachments needs logic that stops dangerous files from being uploaded, and Forminator has exactly that — a dangerous-extension blocklist inside handle_file_upload(). This CVE, though, isn’t a single flaw. It’s a case where two separate weak points happened to line up and together produced RCE.
The first is that the setting determining which file types an upload field accepts gets trusted from the request itself, rather than from the form definition saved on the server. The second is that the blocklist check meant to filter out dangerous extensions compares MIME type strings using exact key matching, with no normalization. Combine the two, and an attacker can inject a forged configuration through a completely different field (Select) to redefine what the upload field accepts, then slip a .php file past the blocklist check with nothing more than a single pipe (|) character.
Where the flaw lives — who decides an upload field’s “allowed types”
Forminator form submissions go through an admin-ajax.php action that’s callable without authentication. In the correct flow, which extensions/MIME types an upload field accepts should be a value the admin configured when building the form, stored on the server (in the database), and the uploaded file should be checked against that stored value at submission time.
// reconstructed from the public advisory — conceptual submission flow
public function handle_file_upload($field, $file, $request_data) {
$allowed_types = $this->get_field_settings($field_id); // this is supposed to be the SAVED setting
...
}
Step-by-step source analysis
Step 1 — the public submission handler trusts field config straight from the request
According to Wordfence’s public advisory, that’s exactly where the problem sits. The public submission handler builds the upload field’s configuration from a value included in the request, rather than from what’s stored on the server — and that value can be injected through a field that looks completely unrelated, like a Select field.
// reconstructed from the public advisory — config merge step before handle_file_upload()
$field_settings = $this->get_field_settings_from_request($_POST);
// embed a structure that looks like an "upload field config" inside the select-1 value,
// and the server accepts it as-is, instead of the admin's actual setting
This step alone is already serious. Even if an admin configured an upload field to “only allow jpg, png,” an attacker sending a crafted value through the Select field can redefine, at request time, which extensions that form actually accepts.
Step 2 — the last line of defense left: the dangerous-extension blocklist
Even if the allowed types can be redefined arbitrarily, handle_file_upload() still has one more check: a separate blocklist filtering out dangerous, server-executable extensions and MIME types like .php or .phtml. This check is where the second flaw lives.
// reconstructed from the public advisory
$dangerous_mime_types = [
'application/x-php' => true,
'application/x-httpd-php' => true,
// ...
];
if (isset($dangerous_mime_types[$mime_key])) {
return new WP_Error('dangerous_file_type', ...);
}
This code only flags something as dangerous when $mime_key is literally identical to one of the blocklist’s keys. In Wordfence’s own words, this logic is bypassed by “pipe-alternative MIME type keys.”
Normal request's mime_key: "application/x-php" → $dangerous_mime_types[...] exists → blocked
Crafted request's mime_key: "application/x-php|" → $dangerous_mime_types[...] doesn't exist → isset() = false → allowed
Appending a single | to the MIME type string is enough to make isset() conclude “no such key exists.” The blocklist’s actual intent — stopping files with dangerous MIME types — goes unenforced, because an implementation that only checks for exact string equality becomes a bypass in its own right.
Step 3 — combine both flaws: a PHP file with attacker-chosen name and content lands on disk
Putting the two flaws together, an attacker can:
- Embed a forged “upload field config” inside the Select field value, overriding the server’s trusted allowed-type list entirely.
- Even if a blocklist check remains, slip past it by appending
|to the MIME type key. - Fully control both the filename and the content.
POST /wp-admin/admin-ajax.php
action=forminator_submit_form_custom-forms-5
file-upload-1[name]=shell.php
file-upload-1[content]=<?php system($_GET['cmd']); ?>
select-1=<encoded value carrying the forged upload-field config>
If this request is processed normally, a PHP file named shell.php, with exactly the content the attacker chose, gets created in the upload directory.
Step 4 — the default directory has .htaccess protection; Custom Upload Storage doesn’t
That’s not the whole story, either. By default, Forminator’s upload directory is protected by a .htaccess file that blocks PHP execution — so even if a .php file gets uploaded, the web server often just returns it as plain text rather than executing it. But when an admin configures a separate “Custom File Upload Storage root,” that protection disappears.
Here’s how Wordfence describes it: that .htaccess file is created “when first needed” — specifically, during the processing of a frontend (unauthenticated) submission request. But the WordPress helper function responsible for writing .htaccess isn’t loaded on that frontend request path at all. As a result, files accumulate in the custom upload path with no PHP-execution safeguard ever having been created.
curl "http://target/wp-content/uploads/forminator-custom/shell.php?cmd=id"
# → uid=33(www-data) gid=33(www-data) groups=33(www-data)
Simply requesting the uploaded file by URL is enough for the server to execute it as PHP. No authentication, no special privileges, no trick on a second request — nothing.
The patch — let the server own the config, and normalize before comparing
Forminator 1.56.2 fixed this structurally. An upload field’s allowed file types are no longer built from values carried in the request at all — only the form configuration stored on the server is trusted. The dangerous-extension/MIME-type blocklist comparison was also changed from a plain key match to a comparison against normalized input, so appending a delimiter like | no longer bypasses it.
Two lessons come out of this. First, the assumption that “this value must already be a server-trusted setting” can be broken from almost any position in a request. If a configuration value can be injected through a field that looks entirely unrelated to the thing it configures — here, a Select field redefining an upload field’s accepted types — the validation built around that setting means nothing. Second, a blocklist or allowlist comparison that checks “is this string exactly equal” without normalizing the input first will always have room for an attacker to construct a variant that means the same thing semantically while differing just enough in the raw string to slip through.
Remediation
- Update Forminator Forms to 1.56.2 or later immediately.
- Check whether any of your forms combine a File Upload field with a Select field, and if an immediate update isn’t possible, consider temporarily disabling the file upload field on those forms.
- If you’re using a “Custom File Upload Storage root,” verify directly — independent of the plugin’s own settings — that web-server-level protection against PHP execution (an Nginx
locationrule, an Apache.htaccessdirective) actually applies to that directory. - If exposure is possible, check both the default and custom upload directories for unfamiliar
.php/.phtmlfiles, and review access logs forforminator_submit_form_*requests toadmin-ajax.phpcarrying unusually largeselectparameter values.


