The Exploit
An authenticated Subscriber-level attacker can delete arbitrary files from the server by sending a specially crafted AJAX request that bypasses path validation in the file deletion routine.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=subscriber_session_token
action=smack_upload_handler&securekey=nonce_value&ftp_file_name=../../../../wp-config.php&upload_type=ftp
The attacker observes an HTTP 200 response with success JSON output, and the target file — wp-config.php, .htaccess, or any other file readable by the web server process — is unlinked from disk. Subsequent requests to the site fail as critical configuration or code is deleted, demonstrating reliable arbitrary file deletion.
What the Patch Did
Before
unlink($path);
After
$upload_dir = wp_upload_dir();
$base_dir = trailingslashit($upload_dir['basedir']);
$real_base = realpath($base_dir);
$real_path = realpath($path);
if ($real_path !== false && strpos($real_path, $real_base) === 0) {
unlink($real_path); // safe delete inside uploads only
}
The patch introduced a path canonicalization and confinement control using realpath() to resolve the supplied path to its absolute, symlink-free form, then compared it against the realpath() of the uploads directory. Only if the resolved path begins with the uploads directory prefix is the file deletion permitted. This prevents path traversal attacks (e.g., ../../../../wp-config.php) from escaping the intended upload sandbox.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The attacker-controlled ftp_file_name parameter flows directly into the $path variable passed to unlink() without validation. The plugin constructed a file path using string concatenation or simple path construction, then attempted deletion without verifying that the resolved path remained within the intended upload directory. Relative path sequences (../) and absolute paths were not stripped or validated, crossing the trust boundary between user input and the filesystem operations boundary unchecked.
Why It Works
The load-bearing line is if ($real_path !== false && strpos($real_path, $real_base) === 0). This single conditional gates the unlink() call. Without it, even if realpath() were called, the deletion would proceed unconditionally. The realpath() calls themselves are necessary but insufficient — a string comparison must enforce the containment invariant. The engineer added the wp_upload_dir() and trailingslashit() calls to establish a canonical baseline directory; without a properly normalized reference point, the prefix check would be bypassable via symlinks or trailing-slash tricks. The !== false guard handles the edge case where realpath() returns false for a nonexistent path, preventing logic confusion.
Hardening Checklist
- Always canonicalize filesystem paths before security decisions: Use
realpath()on both user input and your intended safe directory, then enforce astrpos(..., $safe_dir) === 0prefix check before any filesystem operation (read, write, delete, include). - Whitelist upload locations by directory: Retrieve the uploads directory once via
wp_upload_dir()['basedir']and reject any requested path that does not resolve within it; never allow uploads outside the designated plugin or site directory. - Reject path traversal sequences in filenames: Before path construction, strip or reject filenames containing
..,/,\, or null bytes using regex orbasename()to isolate the filename alone. - Use
wp_verify_nonce()explicitly and checkcurrent_user_can(): The original code relied oncheck_ajax_referer()alone; add explicit capability checks to gate sensitive operations to administrators only, usingcurrent_user_can('manage_options'). - Implement a whitelist of allowed file extensions: Define an array of permitted extensions (e.g.,
['csv', 'xml', 'xlsx']) and reject any upload or processed file that does not match, usingin_array($ext, $whitelist, true)for strict comparison.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-10058