The Exploit
An unauthenticated attacker with network access to an exposed elFinder instance can delete arbitrary files from the server by crafting a directory traversal request that bypasses the weak path sanitization.
POST /wp-content/plugins/advanced-file-manager-plus/includes/application/class_fma_admin_menus.php HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 85
action=afm_delete_file&directory=..%2f..%2f..%2fwp-config.php&nonce=attacker_controlled
The attacker observes a 200 response indicating the file deletion succeeded. The wp-config.php file (or any target file within reach of the web server process) is removed from disk, causing the WordPress installation to become non-functional or exposing credentials if backups were the only recovery vector.
What the Patch Did
Before:
public function afm_sanitize_directory($path = '') {
if(!empty($path)) {
$path = str_replace('..', '', htmlentities(trim($path)));
}
return $path;
}
After:
public function afm_sanitize_directory($path = '') {
if(!empty($path)) {
$path = str_replace('..', '', htmlentities(trim($path)));
$path = wp_normalize_path(realpath($path));
}
return $path;
}
The patch added realpath() to resolve the path to its absolute, canonical form, combined with wp_normalize_path() to standardize the path representation. This transforms a relative path containing traversal sequences into the actual filesystem location it resolves to — immediately exposing whether the resolved path escapes the intended directory boundary. The previous str_replace('..', '') approach is a blacklist that strips only the literal string .., leaving encoded variants (%2e%2e), unicode escapes, symlink traversal, and multi-stage encoding untouched.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The vulnerability flows from the directory parameter in the POST request directly into afm_sanitize_directory() without canonical path resolution. The function attempts to filter .. sequences via string replacement, but this is insufficient because:
- The attacker can URL-encode the dots as
%2eor use other encoding schemes that survive the simple string replacement. - Relative paths are never resolved to their actual filesystem location, so
../../../wp-config.phpremains a valid relative traversal even after..removal — the code never verifies the final path lands within an allowed base directory. - The
str_replace()call operates only on the literal..string, not on path semantics.
The attacker-controlled directory parameter crosses the trust boundary at the point it is passed to file deletion operations without being constrained by realpath() and a subsequent whitelist check against the plugin's designated upload directory.
Why It Works
The load-bearing line is $path = wp_normalize_path(realpath($path));.
If you removed it, the bug persists because str_replace('..', '') alone can be bypassed with encoded traversal sequences. The engineer added realpath() because it is the canonical way to resolve a potentially relative or symlink-containing path to the actual filesystem location the OS will access — short-circuiting all encoding and symbolic tricks. wp_normalize_path() follows it to ensure the returned path uses forward slashes and matches WordPress' internal path conventions, making downstream directory containment checks reliable.
The earlier str_replace() and htmlentities() calls do not achieve path safety; they are remnants of insufficient prior attempts. htmlentities() is a red herring — it encodes HTML entities, not path separators — and reveals the code was conflating output escaping with input validation.
Hardening Checklist
- Use
realpath()+ whitelist containment: After callingrealpath(), usestrpos()orwp_normalize_path()comparison to verify the resolved path starts with the intended base directory. Do not rely on string replacement to block traversal. - Validate against a known-safe base directory: Store the plugin's upload directory as a constant and ensure all file operations occur beneath it. Use
wp_safe_remote_get()or similar APIs that enforce directory containment. - Reject URL-encoded and double-encoded inputs: Before passing user input to filesystem functions, decode it once with
rawurldecode(), then validate. Never trusturldecode()or assume the OS will reject bad paths — validate explicitly. - Use WordPress' file API: Prefer
WP_Filesystemclass methods over bareunlink()orrmdir(), which include path safety checks and respectwp-config.phpsecurity constants. - Implement a capability check: Wrap file deletion in
current_user_can('manage_options')or similar, and verifywp_verify_nonce()on the action — session hijacking or CSRF should not suffice to delete files.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-0818