//nefariousplan

CVE-2026-5718: The Drag-and-Drop CF7 Nonce Mint Blocks One Caller, And Its Name Is curl

patterns

cve

proof of concept

CVE-2026-5718 names an unauthenticated arbitrary file upload in Drag and Drop Multiple File Upload for Contact Form 7, the codedropz plugin that adds drag-and-drop attachment fields to Contact Form 7 on hundreds of thousands of WordPress sites. The vulnerable upload handler is registered for both wp_ajax_dnd_codedropz_upload and wp_ajax_nopriv_dnd_codedropz_upload. Its only identity gate is check_ajax_referer('dnd-cf7-security-nonce', 'security', false).

The same source file registers a second nopriv AJAX action whose entire purpose is to mint a dnd-cf7-security-nonce and return it as JSON. Before issuing the nonce, the function performs one inspection of the request. It checks whether the User-Agent header contains the substring curl.

The plugin maintains its own unauthenticated nonce mint

inc/dnd-upload-cf7.php, lines 32 through 71, in the version that carries CVE-2026-5718:

add_action('wp_ajax__wpcf7_check_nonce', 'dnd_wpcf7_nonce_check');
add_action('wp_ajax_nopriv__wpcf7_check_nonce', 'dnd_wpcf7_nonce_check');

// ...

function dnd_wpcf7_nonce_check() {
    // Block curl request.
    if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'curl' ) !== false ) {
        wp_send_json_error('Request blocked: cURL access is forbidden.');
    }

    if( ! check_ajax_referer( 'dnd-cf7-security-nonce', false, false ) ){
        wp_send_json_success( wp_create_nonce( "dnd-cf7-security-nonce" ) );
    }
}

Three behavioral lines describe the entire authorization model. The first asks whether the User-Agent header contains the four-character substring curl. If it does, the request is rejected with a JSON error. If it does not, control moves on.

The second calls check_ajax_referer with $action = 'dnd-cf7-security-nonce', $query_arg = false (do not look for the nonce in a specific request key), and $die = false (do not call wp_die on failure). The function returns true if any nonce in the request validates against that action, false otherwise.

The third is the only branch most callers ever reach. When check_ajax_referer returns false, which it does for every fresh caller who has no nonce yet, the function calls wp_create_nonce("dnd-cf7-security-nonce") and writes the result back to the caller as a JSON success payload. WordPress nonces are computed against the requesting user ID, the action name, and a 12-hour time slice. For an unauthenticated caller, the user ID is 0. The returned token is valid for the next 24 hours for any caller whose user ID resolves to 0, which is to say, every unauthenticated caller.

A requests.get to /wp-admin/admin-ajax.php?action=_wpcf7_check_nonce with the default python-requests/2.x User-Agent produces this:

{"success":true,"data":"a1b2c3d4e5"}

The string in data is a valid dnd-cf7-security-nonce for every other unauthenticated caller for the next 24 hours, against any caller whose User-Agent is not the literal string curl.

The upload handler verifies the nonce the mint just issued

dnd_upload_cf7_upload(), the handler bound to dnd_codedropz_upload, opens with this:

function dnd_upload_cf7_upload() {
    // ... read $_POST['form_id'], $_POST['upload_name'], settings ...

    // check and verify ajax request
    if( ! check_ajax_referer( 'dnd-cf7-security-nonce', 'security', false ) ) {
        wp_send_json_error('The security nonce is invalid or expired.');
    }
    // ...
}

check_ajax_referer here verifies that the security POST field carries a nonce minted for dnd-cf7-security-nonce. The mint at line 62 issues exactly that nonce, against exactly that action, to exactly that population of callers (user ID 0). The mint and the verifier are paired. They were written to be paired. The pairing is the bug.

current_user_can() is not called anywhere in dnd_upload_cf7_upload. The function proceeds to a long chain of file extension checks, MIME checks, filename sanitization, and finally move_uploaded_file into the WordPress uploads directory under a folder the caller picks via $_POST['upload_folder']. Identity is never tested. The handler trusts that anyone who can present a valid nonce has been authorized somewhere upstream. Upstream is the line above. The end of that chain is the unauth-write-to-execution-path shape: an unauthenticated POST lands a file at a path the same Apache serves with mod_php. Every CVE in this plugin's series is a different way to keep .php in the filename through that chain.

What the 1.3.9.7 patch closes

The May 2026 patch tightens three file-validation paths in the upload handler. The first changes the blacklist setting from a replacement to a merge:

-$blacklist_types = dnd_cf7_not_allowed_ext();
-if ( isset( $blacklist["$cf7_upload_name"] ) && ! empty( $blacklist["$cf7_upload_name"] ) ) {
-    $blacklist_types = explode( '|', $blacklist["$cf7_upload_name"] );
-}
+$blacklist_types = dnd_cf7_not_allowed_ext();
+if ( isset( $blacklist["$cf7_upload_name"] ) && ! empty( $blacklist["$cf7_upload_name"] ) ) {
+    $blacklist_option = explode( '|', $blacklist["$cf7_upload_name"] );
+    $custom_blacklist = array_filter( array_map( 'trim', $blacklist_option ) );
+    $blacklist_types  = array_unique( array_merge( $blacklist_types, $custom_blacklist ) );
+}

In the pre-patch code, an admin who configured a per-form blacklist of exe|scr overwrote the default list ('html', 'svg', 'phar', 'php', 'php3', 'php4', 'pht', 'php5', 'php7', 'php8', ...). When the form's allowed types were set to *, an attacker uploading shell.php cleared the per-form blacklist (which did not contain php) and a second hardcoded fallback list at line 927 that only contained phar, svg, php5, php7, php8. The patch replaces the assignment with array_merge. The patch also expands the wildcard-mode fallback list to include php, php3, php4, pht, phtml:

-$not_allowed_ext    = array( 'phar', 'svg', 'php5', 'php7', 'php8' );
+$not_allowed_ext    = array( 'phar', 'svg', 'php', 'php3','php4', 'pht', 'phtml', 'php5', 'php7', 'php8' );

The CVE description says "arbitrary file upload." The fix line, in 2026, adds the literal string php to the list of file extensions a hardcoded server-side filter for arbitrary file uploads considers dangerous.

The third change removes a conditional around the antiscript filename function:

-$ascii_name = dnd_cf7_remove_icons( $filename );
-if ( dnd_cf7_check_ascii( $ascii_name ) ) {
-    $filename = wpcf7_antiscript_file_name( $ascii_name );
-}
+$filename = dnd_cf7_remove_icons( $filename );
+$filename = wpcf7_antiscript_file_name( $filename );

wpcf7_antiscript_file_name is the WordPress Contact Form 7 helper that strips PHP-script-extension parts (php, phtml, pl, py, rb, cgi, asp, aspx) from multi-part filenames. A filename like shell.php.pdf becomes shell.pdf. Pre-patch, this function was called only when dnd_cf7_check_ascii returned true. A filename containing any non-ASCII byte caused the function to be skipped. Post-patch, it runs unconditionally.

The mint is unchanged

The eleven-line block at the top of inc/dnd-upload-cf7.php is byte-identical between version 1.3.9.6 (vulnerable) and 1.3.9.7 (patched):

add_action('wp_ajax__wpcf7_check_nonce', 'dnd_wpcf7_nonce_check');
add_action('wp_ajax_nopriv__wpcf7_check_nonce', 'dnd_wpcf7_nonce_check');

function dnd_wpcf7_nonce_check() {
    if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'curl' ) !== false ) {
        wp_send_json_error('Request blocked: cURL access is forbidden.');
    }
    if( ! check_ajax_referer( 'dnd-cf7-security-nonce', false, false ) ){
        wp_send_json_success( wp_create_nonce( "dnd-cf7-security-nonce" ) );
    }
}

The patch closes three file-validation gaps in the handler. The endpoint that mints the credential required to reach the handler is in the same source file, registered for unauthenticated callers, and the patch does not touch it. The plugin's idea of who is not authorized to receive a fresh upload nonce is curl.

This is the fifth file-upload security entry in this plugin's changelog

The readme.txt Changelog section, read in chronological order:

= 1.3.9.0 =
- Security: Fixed security issues reported by Wordfence "Remote Code Execution
  via PHAR File Upload if changing the filename something like poc.phar".

= 1.3.9.3 =
- Security: Fixed an unauthenticated limited arbitrary file upload issue
  allowing .phar and .svg files when using blacklist mode with file types
  set to *. (by WordFence via andrea bocchetti)

= 1.3.9.6 =
- Security: Unauthenticated Arbitrary File Upload (Reported by Thomas Sanzey
  via WordFence) - user able to upload "php5 - php8" for non ascii filename
  by bypassing extensions present in the blacklists.

= 1.3.9.7 =
- Security Fix : Unauthenticated Arbitrary File Upload via Non-ASCII Filename
  Blacklist (Thanks to Leonid Semenenko via WordFence)

Four consecutive releases, four file-upload security entries, four different researchers. This is design-debt-driver at the file-validation layer of one plugin. Each release closes the specific bypass the previous researcher reported. The next researcher finds the next bypass. The plugin has not changed its handler architecture (wp_ajax_nopriv_ for the upload, wp_ajax_nopriv_ for the nonce mint, User-Agent string for the mint's only check) across any of those releases.

The 1.3.9.6 entry and the 1.3.9.7 entry are about the same bug class. Sanzey's 1.3.9.6 fix added dnd_cf7_remove_icons to strip emoji, regional indicators, ZWJ, and skin-tone modifiers from the filename before the ASCII check, which addressed the specific bypass Sanzey reported (a ZWJ inside the filename made it non-ASCII and skipped the antiscript step). Sanzey's fix kept the ASCII gate. Semenenko's 1.3.9.7 fix removes the gate. Sanzey closed the door named in the report. Semenenko closed the doorway.

The exploit that ships with the CVE harvests forms before it harvests nonces

The kyukazamiqq/cve-2026-5718 repository on GitHub is two Python scripts and seven markdown files. Four of the markdown files (FORM_ID_VISUAL_GUIDE.md, HOW_FORM_ID_WORKS.md, README_FORM_ID_GUIDE.md, FORM_ID_QUICK_REFERENCE.txt) are zero bytes on disk. They are filename placeholders for a documentation set the author named before they wrote.

The two scripts are discover_forms.py and exploit_cve_2026_5718.py. The first reads a target list, fetches each WordPress homepage, queries /wp-json/contact-form-7/v1/forms, parses /sitemap.xml for indexed pages (the first fifty), and tries seven common contact-page paths (/contact, /contact-us, /get-in-touch, /inquire, /support, /feedback, /contact-form). For each site that responds, it extracts Contact Form 7 form IDs via five separate regex patterns and writes them to discovered_forms.txt in <url>|<form_id>|file_upload format.

The second reads from targets.txt, runs both file-validation bypass attempts against each site with a 0.5-second sleep between hosts, and writes confirmed hits to cve_2026_5718_results.txt in this format:

http://site1.com | Uploaded: shell-abc123.php | 2026-05-08 15:32:45
http://site2.com | Uploaded: shell-def456.php | 2026-05-08 15:32:47

Three columns. URL, deployed-shell filename, timestamp. That is a campaign hit-list, not a debug log. The output banner the script prints declares "CVE-2026-5718 Mass Exploitation Tool, Unauthenticated File Upload."

The README for the kit describes the workflow as targets.txt > discover_forms.py > discovered_forms.txt > exploit_mass.py > successful.txt. The chain is named in full. exploit_mass.py is not in the repository. The QUICK_START.md instructs readers to "create exploit_mass.py for batch exploitation" as the next step. The disclaimer reads "EDUCATIONAL USE ONLY" and lists "vulnerability assessment on systems you own" among the purposes the tool is provided for. A discovery harvester that walks WordPress REST APIs to enumerate Contact Form 7 form IDs and feeds them to a list-mode exploit is not a tool for assessing systems you own. This is the disclaimer-wrapped campaign kit shape, identical to AnggaTechI's pix-woocommerce harvester documented in the prior post on this catalog.

Neither script in the kit uses the _wpcf7_check_nonce mint. exploit_cve_2026_5718.py line 36 scrapes the nonce by regex from the homepage HTML:

nonce_match = re.search(
    r'dnd-cf7-security-nonce["\']?\s*:\s*["\']([a-f0-9]+)["\']',
    resp.text
)

The plugin's frontend script localizes a nonce inline on every page that loads its assets ('ajax_nonce' => wp_create_nonce("dnd-cf7-security-nonce") at line 590 of the source). When the regex finds a hit on the homepage, the upload step has a nonce. When it does not, the script falls back to self.nonce or '' and the upload fails. The _wpcf7_check_nonce endpoint, which would issue a fresh nonce on demand against any target whose User-Agent is not the literal string curl, is not used. The author either did not read past line 33 of the plugin source, or read it and chose not to surface the simpler primitive in the kit.

The non-ASCII bypass the kit ships does not work against the source the kit names

exploit_cve_2026_5718.py line 96 constructs its non-ASCII filename like this:

filename_bypass = f"shell.php\u200d.pdf"

A zero-width joiner between .php and .pdf. The PoC's claim is that the ZWJ makes the filename non-ASCII, which causes dnd_cf7_check_ascii to return false, which skips wpcf7_antiscript_file_name. The filename therefore reaches disk with .php. in the middle.

dnd_cf7_remove_icons at line 1015 of the plugin source strips \x{200D}. The ZWJ is in the strip pattern, alongside the emoji block, regional indicators, variation selectors, and skin-tone modifiers. By the time the ASCII check at line 970 runs, the filename has been normalized to shell.php.pdf, which is ASCII. The antiscript function runs. The php part is removed. The file lands as shell.pdf.

The bypass that does work uses a non-ASCII codepoint outside the strip pattern. A Cyrillic а at U+0430, a Hebrew א at U+05D0, a Greek α at U+03B1, none of these appear in dnd_cf7_remove_icons's pattern. A filename shell.phpа.pdf (Cyrillic а between php and .pdf) survives the strip step with the non-ASCII byte intact, fails the ASCII check, skips the antiscript function, and is written to disk with the .php substring preserved. On Apache configurations that route .php extension matches via AddHandler rather than <FilesMatch \.php$>, the embedded .php triggers PHP execution. The PoC author tested their bypass against a simulated version of the function (lines 39 to 46 of cve_2026_5718_test.php) that omitted the icon-strip step. They did not test against the source the CVE names.

The shape, three plugins on

The plugin's CVE record reads as a file-validation problem. The exploit kit reads as a file-validation problem. The patch reads as a file-validation problem. The handler that lets unauthenticated callers reach those file-validation paths is the same shape we have written about in Pix for WooCommerce CVE-2026-3891, in vczapi CVE-2026-1368, and in the midi-Synth handler family for CVE-2026-1306. Every one of those plugins registers a privileged upload, settings-write, or token-issue handler under wp_ajax_nopriv_. Every one of them uses a nonce check as the gate. Every one of them ships an unauthenticated nonce source somewhere else: a companion wp_ajax_nopriv_ AJAX action whose only purpose is to call wp_create_nonce, or a wp_localize_script block printing the nonce inline on a public page, or, in this plugin, a wp_ajax_nopriv_ AJAX action whose only purpose is to call wp_create_nonce after asking whether the User-Agent contains curl.

This is nonce-is-not-auth. A WordPress nonce is a CSRF token; valid-nonce means fresh-and-server-issued, not authenticated. The dnd-cf7 plugin's dnd_wpcf7_nonce_check is the explicit, named, unauthenticated nonce source the pattern's mechanism description warns against. The author wrote it as a feature. The feature is the bug.

PoC: kyukazamiqq/cve-2026-5718

The plugin's nonce mint blocks one caller. Its name is curl.