The Exploit
An authenticated user with Editor-level access can read arbitrary files on the server by injecting path traversal sequences into the logFile AJAX parameter.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<valid_editor_session>
action=shortpixel_image_optimizer&do=loadLogFile&logFile=../../../etc/passwd&nonce=<valid_nonce>
The server responds with the contents of /etc/passwd. Because the vulnerable code concatenates user input directly into a file path without validating that the result stays within the intended backup directory, an attacker can traverse the directory tree and read sensitive files — database configuration files, WordPress security keys, environment files containing API credentials, or source code.
What the Patch Did
Before:
$logFile = $data['logFile'];
After:
$logFile = $data['logFile'] . '.log';
The patch enforces a strict file extension requirement by appending .log to the user-supplied filename. This breaks path traversal attacks because a request for ../../../etc/passwd becomes ../../../etc/passwd.log, which does not exist and cannot be used to escape the backup directory. The deeper fix in BulkController.php adds realpath() confinement to verify that the resolved path stays within SHORTPIXEL_BACKUP_FOLDER using strpos($resolvedPath, $backupPath) !== 0, but the .log extension enforcement in the AJAX handler is the first line of defence — it prevents the attacker from supplying arbitrary file paths in the first place.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
The logFile parameter arrives unsanitized from $_POST['logFile'] in the AJAX request handler (AjaxController.php line 1679). The AJAX action loadLogFile passes this value directly to the loadLogFile() method in BulkController.php, which concatenates it with the backup directory path: $backupDir->getPath() . $logName. The filesystem API then resolves this path and opens the file without first validating that the result stays within the intended directory. An attacker exploits this by supplying sequences like ../../../etc/passwd that navigate up the directory tree to access files outside the backup folder. No validation occurs at the boundary where user input enters the request or at the sink where the file is opened.
Why It Works
The load-bearing line is $logFile = $data['logFile'] . '.log'; — it appends a fixed extension that the attacker cannot control. Removing it restores the vulnerability because an attacker can then supply ../../../etc/passwd and read that file directly. The engineer also added realpath() validation in BulkController.php (lines 1693–1698) as defence-in-depth: even if an attacker somehow bypasses the .log extension check, realpath() resolves symbolic links and normalizes the path, then strpos($resolvedPath, $backupPath) !== 0 confirms the result stays within the backup directory. Neither defence is sufficient alone — the .log extension stops most traversal attempts at the AJAX layer, while realpath() confinement catches attempts that slip through. Together they form a two-stage filter: input validation (extension) and output validation (path confinement).
Hardening Checklist
- Use
realpath()and path confinement on all file operations. Before opening a file, resolve its path withrealpath()and verify it begins with the intended directory usingstrpos(). This prevents both../traversal and symlink escapes. - Enforce strict file extensions for user-supplied filenames. Append a fixed, non-negotiable extension (e.g.,
.log,.csv) to user input before using it in a path. This breaks traversal sequences that rely on bypassing directory checks. - Whitelist filenames instead of blacklisting traversal sequences. Rather than trying to detect and block
../, define a whitelist of allowed filenames (UUIDs, checksums, or predefined slugs) and reject anything else. Maintain the list server-side, never derive it from user input. - Use WordPress's
wp_safe_remote_get()or file functions withWP_Filesystem_Directand verify caller capabilities. Wrap file access in capability checks (current_user_can()) and log the operation so unauthorized reads are auditable. - Test path traversal during code review. For any function that builds a file path from user input, manually test payloads like
../../../etc/passwd,..%2F..%2F..%2Fetc%2Fpasswd, and....//....//etc/passwd(double-slash encoding). Automated SAST tools often miss these if the validation logic is custom.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-1246