Someone left the loading dock unlocked, and the security camera only covers half the lot
CVE-2026-15748 is an unauthenticated arbitrary file upload in the Forminator Forms WordPress plugin (≤ 1.56.1, 600,000+ active installs). The handle_file_upload() function uses an exact-key blocklist to reject dangerous extensions, but attackers bypass it by submitting the MIME key ph(p)|text/x-php — not an exact match for php, yet WordPress's internal matcher still resolves it to .php. Worse, the public submission handler trusts attacker-controlled configuration injected via a forged Select field value, letting an unauthenticated visitor write a PHP webshell to the uploads directory. The flaw was reported by researcher daroo through the Wordfence Bug Bounty Program on July 11, 2026, patched in 1.56.2 on July 31, and publicly disclosed August 18.
The vendor's CVSS 9.8 CRITICAL is honest about the *theoretical* ceiling — unauthenticated, no interaction, full CIA impact — but it overestimates the *default* exploitability on Apache/LiteSpeed hosts. Forminator ships an .htaccess file in wp-content/uploads/forminator/ that blocks PHP execution, so on a stock Apache setup the uploaded shell is inert even after a successful upload. That protection vanishes in two common scenarios: (1) the site runs nginx, which ignores .htaccess entirely (~30-40% of WordPress hosting), and (2) the administrator configured a custom upload storage root, which may lack the protective .htaccess because it's first initialised during a frontend request where the WordPress helper that creates it isn't loaded. Factoring in the form-configuration prerequisite (both File Upload *and* Select fields must be present) and the Apache default mitigation, a reassessed score of 9.0 CRITICAL better reflects the real-world attack surface while still flagging this as a drop-everything-and-patch issue.
5 steps from start to impact.
Target discovery
wp-content/plugins/forminator/ asset paths and readme.txt version strings. Tools like WPScan, Nuclei, and the purpose-built yora1928/cve-2026-15748 scanner automate this at scale across IP ranges or domain lists. Version ≤ 1.56.1 is flagged as exploitable.- Target runs WordPress with Forminator ≤ 1.56.1
- Plugin assets or readme.txt are publicly accessible
- Sites behind Cloudflare or similar CDN may not expose plugin paths directly
- Some hosts strip version strings from plugin assets
Form reconnaissance
admin-ajax.php.- A public page contains a Forminator form with File Upload + Select fields
- The nonce token is embedded in the page source
- Not every Forminator form has both field types; simple contact forms with only text fields are not exploitable
- Forms behind authentication (member-only pages) narrow the target pool
Payload injection via forged Select field
admin-ajax.php with the Forminator submission action. Inside the Select field's nested value structure, they inject a fake record containing field_type=upload, a custom name, and an attacker-controlled additional-type string set to ph(p)|text/x-php. The set_field_data() function processes this without verifying origin, adding the forged upload config to the internal field_data_array.- Attacker can submit POST requests to /wp-admin/admin-ajax.php
- Valid form ID and nonce are known
- WAFs with WordPress-aware rulesets (Wordfence Premium, Sucuri, Cloudflare WAF) may flag the nested array injection pattern
- Request body containing php-related MIME strings may trigger signature-based detection
field_type=upload in Select field parameters.PHP file upload via MIME type bypass
process_uploads() function encounters the injected record and treats it as a legitimate upload field configuration. The blocklist checks the MIME key against known dangerous extensions using exact matching — ph(p)|text/x-php does not match the key php, so the check passes. WordPress's wp_check_filetype() then resolves the ph(p) pattern back to .php, and the file is written to the uploads directory with a PHP extension. The attacker's webshell is now on disk.- The MIME type bypass is not caught by server-level validation
- File is written to a web-accessible directory
- On Apache with AllowOverride enabled (the default for most cPanel/Plesk WordPress hosts), the
.htaccessfile inwp-content/uploads/forminator/blocks PHP execution — the file lands but cannot be executed - Hardened PHP configurations with
open_basedirordisable_functionslimit post-upload impact
wp-content/uploads/**/*.php.Webshell execution and RCE
https://target/wp-content/uploads/forminator/random-prefix-shell.php). If the web server executes PHP in this directory — as is the case on nginx by default, Apache with AllowOverride disabled, or sites using a custom upload storage root lacking .htaccess — the webshell runs as the web server user (www-data/apache/nginx). From here, the attacker reads wp-config.php for DB credentials, adds admin users, installs backdoors, or pivots.- Web server executes PHP in the uploads directory (nginx, misconfigured Apache, custom upload path)
- Uploaded file is web-accessible
- Apache + default .htaccess: PHP execution is blocked — chain dies here for ~60% of installs
- Managed WordPress hosts (WP Engine, Kinsta, Flywheel) typically deny PHP execution in uploads at the platform level
- Some nginx configs include deny rules for PHP in wp-content/uploads
wp plugin update forminator or enable WordPress auto-updates for the plugin. Verify post-update with the version check script. This eliminates the vulnerability entirely.location ~* /wp-content/uploads/.*\.php$ { deny all; } to your server block. For Apache: ensure wp-content/uploads/.htaccess contains <FilesMatch "\.(php|phtml|phar|php[0-9])$">Require all denied</FilesMatch>. Deploy within 3 days. This breaks the RCE chain at step 5 — the shell uploads but cannot execute — regardless of Forminator version.admin-ajax.php where the body contains field_type=upload in non-file-upload field parameters, or where MIME type strings contain php, phtml, or phar patterns. Deploy within 3 days. This blocks the injection at step 3 of the attack chain.wp plugin deactivate forminator on any site where the plugin is installed but not required. Many WordPress installations accumulate unused plugins. Deactivation removes the vulnerable endpoint entirely. Deploy within 3 days.- WordPress login protection / admin authentication — the vulnerable endpoint
admin-ajax.phphandles unauthenticated requests by design; requiring login on/wp-admin/does not block the attack path. - File-rename or randomize-filename plugins — the MIME type bypass operates at the validation layer *before* any filename transformation; the file still lands with a
.phpextension regardless of name randomization. - Rate limiting on admin-ajax.php — a single POST request is sufficient for full exploitation; throttling does not prevent the attack, only slows mass scanning.
- WordPress nonce verification alone — the nonce is extracted from the public form page by the attacker's scanner; it is not a secret when the form is publicly rendered.
The supporting signals.
| In-the-wild exploitation | No confirmed active exploitation. CISA classified the attack as "automatable" but reported exploitation status as "none" at disclosure (2026-08-18). No KEV listing. However, 51 days post-disclosure with public PoCs — opportunistic scanning for this class of WordPress vuln is virtually certain. No campaign-specific IOCs published. |
|---|---|
| Proof-of-concept availability | Multiple public PoCs. GitHub: yora1928/cve-2026-15748 (automated scanner with nonce extraction and safe upload test, by researcher Yora), ubaydev/CVE-2026-15748 (full exploit). Nuclei templates available via topscoder/nuclei-wordfence-cve. Exploitation barrier is effectively zero. |
| EPSS | 0.06138 (6.14%) — places this well above the median and in approximately the top 5th percentile of scored CVEs. Consistent with the unauthenticated-RCE-in-popular-plugin profile. |
| KEV status | Not listed as of 2026-10-08. Absence does not indicate safety — KEV lags real-world exploitation for WordPress plugin vulns. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — Network attack vector, low complexity, no privileges, no user interaction, unchanged scope, all three CIA impacts high. This is the maximum-severity profile for a single-component vuln. |
| Affected versions | Forminator Forms ≤ 1.56.1 (all versions). Plugin has 600,000+ active installations per WordPress.org; approximately 300,000 remain unpatched based on download telemetry reported by SecurityWeek. |
| Fixed versions | 1.56.2 (released July 31, 2026). Available via WordPress Dashboard auto-update or wp plugin update forminator. No distro backports — this is a WordPress plugin, not a system package. |
| Scanning / exposure | No specific GreyNoise tags or Shodan dorks published for this CVE as of reporting. FOFA/Censys queries for forminator in WordPress plugin paths return hundreds of thousands of results. The yora1928 scanner tool enables mass reconnaissance. |
| Disclosure timeline | Reported to Wordfence Bug Bounty: 2026-07-11. Patch released: 2026-07-31 (20-day turnaround). Public disclosure: 2026-08-18. Days since disclosure: 51. |
| Reporting researcher | daroo, via the Wordfence Bug Bounty Program. Technical writeup published by The Hacker News and SOC Prime. |
Sources.
- The Hacker News — Forminator RCE Analysis
- SecurityWeek — 300,000 WordPress Sites Exposed
- SecurityArsenal — Detection, Hunting, and Remediation Guide
- SOC Prime — CVE-2026-15748 Analysis
- DEV Community — Technical Root Cause Writeup
- GitHub PoC — yora1928/cve-2026-15748
- GitHub PoC — ubaydev/CVE-2026-15748
- NVD — CVE-2026-15748 Detail
Why this verdict
- Unauthenticated remote RCE with zero interaction: The attack vector is as bad as it gets — network-reachable, no authentication, no user click, low complexity. Any internet-facing Forminator form with the right field combination is a target. This alone anchors the score in CRITICAL territory.
- Public weaponized tooling eliminates skill barrier: Two GitHub PoC repos and Nuclei templates mean any script kiddie with a domain list can mass-scan and exploit. The yora1928 scanner automates the entire chain including form crawling, nonce extraction, and upload testing. Exploitation is point-and-shoot.
- nginx exposure gap (~30-40% of WordPress hosting): The default
.htaccessprotection ONLY works on Apache and LiteSpeed. On nginx — which powers a substantial minority of WordPress deployments including many high-traffic sites —.htaccessis completely ignored. Uploaded PHP files execute immediately. This means the 'default mitigation' protects roughly 60% of the install base, leaving ~100,000+ unpatched sites fully exploitable. - Apache .htaccess default protection (downward -0.4): On stock Apache/LiteSpeed configurations (cPanel, Plesk, most shared hosting), Forminator places an
.htaccessin the uploads directory that denies PHP execution. The uploaded shell lands but is inert. This genuinely breaks the chain for the majority of WordPress hosting configurations. - Form configuration prerequisite (downward -0.4): Exploitation requires a form with *both* File Upload and Select fields. While this combination is common in application/registration/support forms, simple contact-us forms with only text inputs are not exploitable. This provides moderate but real friction.
- Role multiplier: WordPress is a web application, not a canonically high-value infrastructure component. (a) *Low-value role*: personal blogs, dev staging sites — compromise yields defacement/spam, minimal blast radius. (b) *Typical role*: corporate marketing site, customer portal, small ecommerce — compromise yields webshell + database (PII, credentials), host-level blast radius. (c) *High-value role*: WooCommerce store processing payments, enterprise intranet, multisite network hosting dozens of tenants — compromise yields payment data, session tokens, and potential lateral movement into the corporate network. For an enterprise managing 10K hosts with 5-20 WordPress instances, these are typically internet-facing and thus directly exposed. The blast radius per-instance is host+database, not domain/fleet/supply-chain. WordPress does not meet the canonical high-value-role threshold (not a DC, hypervisor, IdP, or CI/CD), so no floor override applies — but the unauthenticated-RCE-from-internet profile independently sustains CRITICAL.
Why not higher?
The score is already CRITICAL. The two friction points — Apache .htaccess default protection breaking the RCE chain for ~60% of installations, and the form-configuration prerequisite eliminating forms without File Upload + Select fields — provide genuine, measurable narrowing of the exploitable population. A 9.8 would imply every Forminator instance is trivially exploitable, which is not the case for stock Apache deployments.
Why not lower?
Unauthenticated remote RCE with public exploit tooling on 600,000+ internet-facing installations is the textbook definition of CRITICAL. The nginx exposure alone puts ~100,000+ unpatched sites at immediate risk with zero additional conditions beyond finding the right form. EPSS of 6.14% confirms elevated exploitation probability. Dropping to HIGH would require evidence that the exploitable fraction is negligibly small, which the nginx install-base data directly contradicts. The 51-day window since disclosure with no patch adoption mandate makes this a live, active-surface issue.
Crowdsourced verification payload.
Run this script on each WordPress host (or against a mounted filesystem) as any user with read access to wp-content/plugins/. Usage: bash cve-2026-15748-check.sh /var/www/html. No elevated privileges required for the version check; nginx detection uses pgrep which may need appropriate permissions.
#!/bin/bash
# CVE-2026-15748 — Forminator Forms Arbitrary File Upload Checker
# Outputs: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -uo pipefail
WP_ROOT="${1:-/var/www/html}"
VULN_CEILING="1.56.1"
FIXED="1.56.2"
PLUGIN_FILE="$WP_ROOT/wp-content/plugins/forminator/forminator.php"
if [ ! -f "$PLUGIN_FILE" ]; then
echo "UNKNOWN — Forminator plugin not found at $WP_ROOT"
exit 2
fi
# Extract version from plugin header
VERSION=$(grep -iP '^\s*\*\s*Version:\s*' "$PLUGIN_FILE" 2>/dev/null \
| head -1 | sed 's/.*[Vv]ersion:\s*//;s/[[:space:]]*$//')
if [ -z "$VERSION" ]; then
echo "UNKNOWN — could not parse Forminator version from $PLUGIN_FILE"
exit 2
fi
echo "[*] Forminator version detected: $VERSION"
# Version comparison: is $VERSION <= $VULN_CEILING?
HIGHEST=$(printf '%s\n%s' "$VERSION" "$VULN_CEILING" | sort -V | tail -1)
if [ "$HIGHEST" = "$VULN_CEILING" ] || [ "$VERSION" = "$VULN_CEILING" ]; then
# Additional risk assessment
echo "[!] VULNERABLE — Forminator $VERSION <= $VULN_CEILING (CVE-2026-15748)"
echo " Fix: upgrade to >= $FIXED"
# Check web server type
if command -v nginx >/dev/null 2>&1 && pgrep -x nginx >/dev/null 2>&1; then
echo " [CRITICAL RISK] nginx detected — .htaccess does NOT prevent PHP execution in uploads"
fi
# Check .htaccess in uploads
UPLOAD_DIR="$WP_ROOT/wp-content/uploads/forminator"
if [ -d "$UPLOAD_DIR" ]; then
if [ -f "$UPLOAD_DIR/.htaccess" ]; then
echo " [MITIGATED on Apache] .htaccess found in uploads dir — PHP execution blocked on Apache/LiteSpeed"
else
echo " [HIGH RISK] No .htaccess in $UPLOAD_DIR — PHP execution likely possible"
fi
fi
# Check for existing webshells
if [ -d "$UPLOAD_DIR" ]; then
SHELLS=$(find "$UPLOAD_DIR" -name '*.php' -o -name '*.phtml' -o -name '*.phar' 2>/dev/null | head -5)
if [ -n "$SHELLS" ]; then
echo " [ALERT] Suspicious PHP files found in uploads directory:"
echo "$SHELLS" | while read -r f; do echo " $f"; done
fi
fi
exit 1
else
echo "[+] PATCHED — Forminator $VERSION is not affected by CVE-2026-15748"
exit 0
fi