The Exploit
An unauthenticated attacker can delete arbitrary directories on the server by submitting a specially crafted POST request that traverses out of the intended upload directory.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: vulnerable.wordpress.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 95
action=omgf_admin_notice&nonce=&omgf_remove_folder=../../../../../../var/www/html/wp-content
The attacker observes a successful deletion when wp-content and its subdirectories are removed from the filesystem. No admin credentials are required; the admin_init hook fires for all visitors, and the vulnerable update_settings() function lacks both nonce validation and capability checks.
What the Patch Did
Before
$dir = OMGF_UPLOAD_DIR . '/' . $dir_to_remove;
$this->delete_files( $dir );
After
$dir = OMGF_UPLOAD_DIR . '/' . $dir_to_remove;
if ( $dir !== realpath( $dir ) ) {
continue;
}
$this->delete_files( $dir );
The patch introduces a path confinement check using realpath(), which resolves symbolic links and relative path sequences (../) to their absolute canonical pathname. By comparing the constructed directory path against its real, resolved form, the code ensures that $dir_to_remove cannot contain traversal sequences that would escape OMGF_UPLOAD_DIR. If the two values differ—indicating an attempt to break out of the intended boundary—the delete operation is skipped via continue.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The $dir_to_remove parameter arrives unsanitized from the POST request body into the update_settings() function. The attacker controls this value directly via the omgf_remove_folder query parameter, which is passed to the vulnerable code path without input validation or canonicalization. The function concatenates this untrusted string directly into a filesystem path using simple string concatenation (OMGF_UPLOAD_DIR . '/' . $dir_to_remove) and immediately passes the result to delete_files(), which recursively deletes the directory. The trust boundary—between user input and filesystem operations—is crossed without verification that the final path remains within the upload directory boundary.
Why It Works
The load-bearing line is if ( $dir !== realpath( $dir ) ). Removing this single line restores full exploitability: an attacker's ../../../var/www payload would still construct a concatenated path that, while syntactically invalid for directory operations, would fail silently or delete unintended locations when the OS normalizes the path during the actual filesystem call. The realpath() function forces the kernel-level path normalization to happen before the delete operation, not during it, giving the code a chance to inspect the resolved result. The continue statement that follows is defensive scaffolding—it ensures that if the check fails, the iteration simply skips that directory rather than allowing the flawed path to reach delete_files(). Together, they form a whitelist: only paths that resolve to their constructed form are permitted to proceed.
Hardening Checklist
-
Validate directory names against a whitelist. Before concatenating user input into a filesystem path, use
in_array( $dir_to_remove, $allowed_dirs, true )to ensure the value matches a predefined list of safe directory names, rather than relying on path canonicalization alone. -
Apply
realpath()and strict equality checks on all filesystem operations. For any function that deletes, reads, or writes files, canonicalize the final path withrealpath()and compare it against the intended parent directory usingstrpos( realpath( $path ), realpath( $parent_dir ) ) === 0to verify containment. -
Add nonce verification to all admin_init hooks. Use
wp_verify_nonce( $_REQUEST['nonce'], 'omgf_settings_nonce' )andcheck_admin_referer()to ensure that settings modifications originate from authenticated admin sessions, not external requests. -
Implement
current_user_can()capability checks. Even when callable fromadmin_init, guards likeif ( ! current_user_can( 'manage_options' ) ) return;ensure that only privileged users can trigger sensitive actions. -
Use strict
wp_safe_remote_post()andwp_remote_get()for external paths. If the plugin constructs paths based on user input, wrap filesystem operations in try-catch blocks and log failed operations for later audit.
References
- https://nvd.nist.gov/vuln/detail/CVE-2023-6600