The Exploit
An authenticated WordPress user with subscriber-level access (or above) can delete arbitrary files on the server by sending a specially crafted AJAX request that bypasses path traversal validation.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in_*=<valid_subscriber_session>
action=sonaar_remove_temp_files&nonce=<valid_nonce>&is_temp=1&file=../../../../wp-config.php
The server processes the request without checking whether the subscriber has permission to delete files. The path traversal sequence ../../../../ is not stripped; it passes through the strpos() check because the logic only validates that the normalized path starts with the peaks directory after substitution — but that substitution itself is vulnerable to bypass. An attacker observes a HTTP 200 response with no error message. The target file (wp-config.php, or any other file readable by the web server user) is deleted from disk, potentially leading to remote code execution when critical configuration is removed.
What the Patch Did
Before:
public function removeTempFiles(){
check_ajax_referer('sonaar_music_admin_ajax_nonce', 'nonce');
$is_temp = filter_input(INPUT_POST, 'is_temp', FILTER_VALIDATE_BOOLEAN);
$file = filter_input(INPUT_POST, 'file', FILTER_SANITIZE_STRING);
if ($is_temp && $file) {
$upload_dir = wp_get_upload_dir();
$peaks_dir = $this->get_peak_dir();
$file_path_temp = str_replace($upload_dir['baseurl'] . $this->get_peak_dir(true), $peaks_dir, $file);
if (strpos($file_path_temp, $peaks_dir) === 0 && file_exists($file_path_temp)) {
wp_delete_file($file_path_temp);
}
}
}
After:
public function removeTempFiles($called_from_internal = false){
if (!current_user_can('manage_options')) {
return;
}
if (!$called_from_internal) {
check_ajax_referer('sonaar_music_admin_ajax_nonce', 'nonce');
}
$is_temp = filter_input(INPUT_POST, 'is_temp', FILTER_VALIDATE_BOOLEAN);
$file = filter_input(INPUT_POST, 'file', FILTER_SANITIZE_SPECIAL_CHARS);
if ($is_temp && $file) {
$upload_dir = wp_get_upload_dir();
$peaks_dir = $this->get_peak_dir();
$file_path_temp = str_replace($upload_dir['baseurl'] . $this->get_peak_dir(true), $peaks_dir, $file);
$file_path_temp = wp_normalize_path($file_path_temp);
$file_path_temp = preg_replace('~/\.\./~', '/', $file_path_temp);
if (strpos($file_path_temp, wp_normalize_path($peaks_dir)) === 0 && file_exists($file_path_temp)) {
wp_delete_file($file_path_temp);
}
}
}
The patch introduced two critical security controls: first, a capability check using current_user_can('manage_options') that restricts the function to administrators only, blocking the subscriber-level attack vector entirely. Second, it added path normalization and traversal sanitization via wp_normalize_path() and an explicit regex that strips ../ sequences, combined with a normalized comparison of the final path against the allowed directory. The input filter was also tightened from FILTER_SANITIZE_STRING to FILTER_SANITIZE_SPECIAL_CHARS, though the regex is the actual load-bearing defence.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') combined with CWE-862: Missing Authorization. The file parameter entered via POST without any capability check (current_user_can() was absent), reaching the path-building logic in removeTempFiles(). The original code attempted to confine the deletion to a peaks directory via strpos() check, but this check operated on a path that had been only partially sanitized. An attacker could craft file=../../../../wp-config.php, and after the str_replace() operation (which maps the upload URL to a filesystem path), the traversal sequences remained intact. The strpos($file_path_temp, $peaks_dir) === 0 check would fail to catch this because the path after substitution could still contain ../ tokens that point outside the peaks directory. The absence of wp_normalize_path() and explicit traversal stripping meant the validation logic operated on a denormalized path, allowing the bypass.
Why It Works
The single load-bearing line is $file_path_temp = preg_replace('~/\.\./~', '/', $file_path_temp); — without it, an attacker can still pass ../ through the strpos() check by crafting a URL-encoded or path-relative payload that the simple string comparison does not detect. The wp_normalize_path() call on line before it resolves relative path components and removes redundant slashes, making the input canonical; the regex then surgically removes any remaining .. sequences. The final normalized comparison strpos($file_path_temp, wp_normalize_path($peaks_dir)) === 0 ensures both sides are in canonical form, eliminating case and separator variations. However, without the capability check, none of this matters — a subscriber can still call the function. The engineers added both controls for defence-in-depth: capability check stops the attacker at the gate; path sanitization stops a compromised admin or a concurrent vulnerability that bypasses the capability layer.
Hardening Checklist
-
Always check capabilities before AJAX handlers that modify state. Add
if (!current_user_can('manage_options')) { wp_die(); }at the top of any function that deletes, writes, or updates files. Do not rely on nonce checks alone; they verify intent, not permission. -
Normalize and validate file paths with
wp_normalize_path()before anystrpos()orrealpath()check. Denormalized paths can bypass simple string comparisons; normalization must precede confinement validation. -
Explicitly strip traversal sequences with regex or
realpath()comparison. Usepreg_replace('~/\.\./~', '/', $path)or compare the realpath of the final file against the realpath of the allowed directory to defeat both URL-encoded and literal../attacks. -
Use
FILTER_SANITIZE_SPECIAL_CHARSorsanitize_file_name()for file input, notFILTER_SANITIZE_STRING. String sanitization does not remove path separators; file-specific filters do. -
Perform final existence and permission checks with the normalized path. After constructing the final path, call
file_exists()andis_file()on the normalized result, not on the intermediate substituted path, to ensure the attacker cannot exploit TOCTOU races.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-7856