The Exploit
An authenticated WordPress user with Contributor access or above can include and execute arbitrary PHP files by manipulating the style, style_desktop, or style_mobile parameters across multiple plugin endpoints. Below is a request that triggers Local File Inclusion in the chat styles handler:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Cookie: wordpress_logged_in=<valid_contributor_session>
Content-Type: application/x-www-form-urlencoded
action=ht_ctc_chat_styles&style=../../../../wp-config&style_desktop=default&style_mobile=default
The attacker observes PHP execution: if wp-config.php exists in the WordPress root, its contents (including database credentials and security keys) are included and parsed as PHP. By uploading a malicious file to the uploads directory and traversing to it (e.g., style=../../uploads/malicious), the attacker achieves arbitrary code execution within the web server context.
What the Patch Did
Before:
// call style
$path = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style. '.php';
$path_d = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style_desktop. '.php';
$path_m = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style_mobile. '.php';
After:
// call style
$style = sanitize_file_name( $style );
$path = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style. '.php';
$style_desktop = sanitize_file_name( $style_desktop );
$path_d = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style_desktop. '.php';
$style_mobile = sanitize_file_name( $style_mobile );
$path_m = plugin_dir_path( HT_CTC_PLUGIN_FILE ) . 'new/inc/styles/style-' . $style_mobile. '.php';
The patch applied sanitize_file_name() — a WordPress core function that strips directory traversal sequences (../, ..\), null bytes, and other filesystem metacharacters from filenames. This function removes the semantic meaning of path traversal payloads, converting ../../../../wp-config into wp-config before concatenation. The fix was applied uniformly across six vulnerable files: prev/inc/commons/styles.php, new/inc/group/class-ht-ctc-group-shortcode.php, new/inc/share/class-ht-ctc-share-shortcode.php, prev/inc/class-ccw-shortcode.php, new/inc/chat/class-ht-ctc-chat.php, and new/inc/share/class-ht-ctc-share.php.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
The vulnerability exists in the data flow: user-supplied style, style_desktop, and style_mobile parameters (originating from AJAX POST requests or URL query strings) are read directly into PHP variables without validation. These unsanitized variables are then concatenated into filesystem paths passed to include() and require() statements. The trust boundary failure occurs at concatenation time — the plugin assumes that any string received from the HTTP layer is safe to embed in a path, without accounting for the attacker's ability to inject sequences like ../ that are meaningful to the filesystem. When include() resolves the path, the traversal sequences are interpreted by the OS, allowing the attacker to escape the intended styles/ or styles-list/ subdirectory and reach parent directories, WordPress configuration files, or uploaded malicious PHP.
Why It Works
The load-bearing line is sanitize_file_name(). Without this function call, the traversal payload remains intact through concatenation and reaches include() semantically unmodified. If you removed the sanitize_file_name() call, the bug is immediately exploitable again — the attacker's ../ sequences pass straight through to the filesystem layer.
The engineer correctly applied the fix to all three style variables ($style, $style_desktop, $style_mobile) rather than just one, because each variable is independently concatenated into a file path and included. However, sanitize_file_name() alone is a permissive control: it removes ../ but does not whitelist allowed values. A more robust mitigation would have been an explicit check against a list of known, legitimate style names (e.g., ['default', 'dark', 'compact']) before path construction, or wrapping the final path in realpath() and asserting it stays within the plugin directory. The applied patch stops the immediate attack — path traversal sequences are neutered — but does not prevent a determined attacker from exploiting other vulnerabilities in the same code path, such as Local File Inclusion through symlinks if the server permits them.
Hardening Checklist
- Use a whitelist of allowed values. Instead of sanitizing, validate that
$stylebelongs to a predefined array of safe names viain_array()before path construction. This eliminates the attack surface entirely. - Apply
basename()as an extra layer. Aftersanitize_file_name(), callbasename()on the final constructed path and compare it to the expected filename to ensure no directory components remain. - Use
realpath()confinement. After building the path, resolve it withrealpath()and assert that the returned absolute path starts with the plugin's known styles directory usingstrpos(), preventing symlink escapes. - Implement capability checks at the AJAX boundary. Verify
current_user_can('manage_options')or a more restrictive capability before processing style parameters, reducing the attacker's authentication scope. - Audit all
include()/require()calls in the codebase. Use grep or a static analysis tool to find every instance of dynamic path construction feeding into file inclusion functions, and apply the same fixes retrospectively.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-3849