The Exploit
An authenticated subscriber-level user can download arbitrary files from the server by calling the mercadopagoDownloadLog function with a path-traversal payload in the filename parameter.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<valid_subscriber_session>
action=mercadopagoDownloadLog&filename=../../../../etc/passwd
The server responds with the contents of /etc/passwd as a file download. An attacker observes a 200 status code and receives the file contents directly in the response body, allowing exfiltration of sensitive system files, configuration files containing database credentials, or private keys. The plugin performs no authorization check before processing the download request, even though the underlying file validation logic exists.
What the Patch Did
Before
public function __construct(Logs $logs)
{
$this->logs = $logs;
$this->pluginLogs = $this->getNameOfFileLogs();
}
private function validateFilename(string $filename): bool
{
return $this->hasAllowedExtension($filename) &&
$this->hasNoDisallowedCharacters($filename) &&
$this->containsExpectedTerms($filename);
}
After
/**
* @var CurrentUser
*/
private $currentUser;
public function __construct(Logs $logs, CurrentUser $currentUser)
{
$this->currentUser = $currentUser;
$this->logs = $logs;
$this->pluginLogs = $this->getNameOfFileLogs();
}
private function validatesDownloadSecurity(string $filename): bool
{
$this->currentUser->validateUserNeededPermissions();
return $this->hasAllowedExtension($filename) &&
$this->hasNoDisallowedCharacters($filename) &&
$this->containsExpectedTerms($filename);
}
The patch added a CurrentUser dependency injected through the constructor and integrated a permission validation call ($this->currentUser->validateUserNeededPermissions()) into the filename validation method. This control enforces that the current user possesses the required capabilities before any file-download logic executes. The renamed method from validateFilename to validatesDownloadSecurity signals the broader scope of checks now performed.
Root Cause
CWE-284: Improper Access Control (Missing Authorization Check) combined with CWE-22: Path Traversal. The mercadopagoDownloadLog AJAX handler accepts a filename parameter from any authenticated user and passes it to the Downloader class without verifying that the current user holds the required administrative or capability-based permission to download logs. The filename validation logic itself (extension checks, disallowed-character filters, expected-term matching) was present but insufficient: it only validated the structure of the filename, not whether the requester was authorized to invoke the download. An authenticated subscriber could reach the sink by submitting a crafted filename parameter through the WordPress AJAX handler, bypassing the missing authorization boundary.
Why It Works
The load-bearing line is $this->currentUser->validateUserNeededPermissions(). If removed, the bug remains exploitable: any subscriber with a valid session can still call the download function. The other checks—extension, character filtering, and term matching—are defense-in-depth countermeasures against path traversal within the allowed log directory, but they do not restrict who can invoke the download. The constructor injection of CurrentUser ensures the object is available when needed, and the method rename clarifies that security (not just format validation) is being performed. Without the validateUserNeededPermissions() call, the subscriber's request never fails, and the attacker receives the file contents.
Hardening Checklist
- Inject capability-checking dependencies into class constructors and call them early in any sensitive operation. Do not rely on inline
current_user_can()calls that developers may forget; make the check a required dependency. - Use WordPress capability checks (
current_user_can()) on every AJAX handler that performs privileged actions. Verify the correct capability name (e.g.,manage_optionsfor admin-only downloads) before processing the request. - Rename methods to reflect their security contracts. Changing
validateFilename()tovalidatesDownloadSecurity()signals to future maintainers that authorization is part of the validation logic, reducing the risk of accidental bypass. - Apply path confinement using
realpath()and substring matching to ensure that even if authorization is checked late, resolved file paths cannot escape the intended directory. Comparerealpath($requested_file)againstrealpath($allowed_directory)with a prefix check. - Unit test authorization failures explicitly. Write tests that confirm a subscriber-level user receives a permission-denied error (403 or exception) when attempting to download logs, not a file payload.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-3934