The Exploit
An authenticated subscriber can delete arbitrary files on the server by embedding a path traversal sequence in a forum post, then deleting that post.
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=wpforo_delete_post&post_id=123&nonce=<valid_nonce>&body=[attachment=../../../../../../etc/passwd]
When the attacker submits this request, the forum post containing the crafted attachment reference is stored. Upon deletion via the post management interface, the plugin's attachment cleanup routine processes the [attachment=...] tag embedded in the post body. The regex extracts ../../../../../../etc/passwd directly into $filename without path traversal validation, constructs an absolute path by concatenating it to the attachments directory, and then unlink() deletes whatever file that path resolves to—in this case, /etc/passwd or any other file the web server process can write.
What the Patch Did
Before:
$filename = trim( $attachment[1] );
$file = WPF()->folders['default_attachments']['dir'] . DIRECTORY_SEPARATOR . $filename;
if( file_exists( $file ) ) {
After:
$filename = trim( $attachment[1] );
$filename = str_replace( [ '../', './', '\\' ], '', $filename );
$file = WPF()->folders['default_attachments']['dir'] . DIRECTORY_SEPARATOR . $filename;
$real_file = realpath( $file );
$real_dir = realpath( WPF()->folders['default_attachments']['dir'] );
if( ! $real_file || ! $real_dir || strpos( $real_file, $real_dir . DIRECTORY_SEPARATOR ) !== 0 ) {
continue;
}
if( file_exists( $file ) ) {
The patch adds three defense layers: basic string sanitization via str_replace() to strip common traversal sequences (../, ./, \\); canonical path resolution using realpath() to convert both the computed file path and the allowed attachment directory to their absolute, symlink-resolved forms; and a boundary assertion that verifies the canonical file path starts with the canonical directory path, ensuring the file cannot escape the attachment folder. If any check fails, the loop continues to the next attachment, preventing the deletion.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
The vulnerability lies in the dataflow from user input to file deletion. When a forum post is created, the post body—user-controlled input—is stored in the database. When the post is later deleted, the plugin extracts attachment references using regex matching against $post['body']. The captured filename is placed directly into $filename with only a trim() call, crossing the trust boundary into the file operations subsystem without validating that the resulting path remains within WPF()->folders['default_attachments']['dir']. An attacker with subscriber-level permissions can craft a post body containing [attachment=../../../sensitive_file], and upon post deletion, this traversal sequence is passed unchecked to file_exists() and subsequently to unlink(), allowing access to arbitrary files on the filesystem.
Why It Works
The load-bearing line is the boundary check: strpos( $real_file, $real_dir . DIRECTORY_SEPARATOR ) !== 0. Without it, the other mitigations fail. The str_replace() sanitization is insufficient alone—an attacker can use ..\ (backslash on Windows), URL-encoded sequences, or null bytes to bypass it. The realpath() calls are necessary but toothless without the boundary assertion; they only provide a canonical representation against which to measure. The boundary check enforces that the file's absolute path begins with the directory's absolute path, making it mathematically impossible for a traversal sequence to reach a parent directory. Removing this line would leave the vulnerability exploitable via encoding bypass. The engineer added the sanitization as a first-pass filter to catch obvious attacks early and reduce log noise; they added the realpath() calls to eliminate symlink-based escapes (where a symlink in the attachment directory points outside it); and they combined all three because defense-in-depth ensures that failure of any single technique does not restore exploitability.
Hardening Checklist
- Use
realpath()+ boundary assertion on all user-supplied file paths before passing them to file operations. Never concatenate user input directly into paths without canonical resolution and containment checks. - Apply
wp_safe_remote_get()orwp_safe_remote_post()for any HTTP requests triggered by untrusted data, and validate the$responsebefore using its body. - Implement a whitelist-based attachment validator that checks file extensions and MIME types against a hardcoded list of permitted attachments, rejecting anything not explicitly allowed.
- Use
wp_attachment_is_image()andwp_get_attachment_metadata()to validate uploaded files through WordPress's native attachment API rather than custom file handling. - Audit all regex patterns that extract filenames or paths from user input; ensure captured groups are validated against CWE-22 patterns before use in file operations. Consider using
basename()to strip directories entirely if only the filename is needed.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-3666