CVE-2026-2257

GetGenie <= 4.3.2 - Insecure Direct Object Reference to Authenticated (Author+) Stored Cross-Site Scripting via REST API

mediumAuthorization Bypass Through User-Controlled Key
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
4.3.3
Patched in
1d
Time to patch

Description

The GetGenie plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 4.3.2 due to missing validation on a user controlled key in the `action` function. This makes it possible for authenticated attackers, with Author-level access and above, to update post metadata for arbitrary posts. Combined with a lack of input sanitization, this leads to Stored Cross-Site Scripting when a higher-privileged user (such as an Administrator) views the affected post's "Competitor" tab in the GetGenie sidebar.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.3.2
PublishedMarch 12, 2026
Last updatedMarch 13, 2026
Affected plugingetgenie

What Changed in the Fix

Changes introduced in v4.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2257 (GetGenie IDOR to Stored XSS) ## 1. Vulnerability Summary The **GetGenie** plugin (<= 4.3.2) contains an Insecure Direct Object Reference (IDOR) vulnerability in its REST API implementation. The `GenieAi\App\Api\Store::action` function handles metadata up…

Show full research plan

Exploitation Research Plan: CVE-2026-2257 (GetGenie IDOR to Stored XSS)

1. Vulnerability Summary

The GetGenie plugin (<= 4.3.2) contains an Insecure Direct Object Reference (IDOR) vulnerability in its REST API implementation. The GenieAi\App\Api\Store::action function handles metadata updates but fails to verify if the authenticated user has permission to edit the specific post identified by the post_id parameter. While it checks for the publish_posts capability (typically held by Authors and above), it does not restrict updates to the user's own posts.

Furthermore, the data stored in the serpData key is not sanitized before being saved via update_post_meta. This allows an attacker to inject malicious scripts into post metadata. When a higher-privileged user (e.g., an Administrator) views the "Competitor" tab in the GetGenie sidebar while editing the affected post, the script executes in their session (Stored XSS).

2. Attack Vector Analysis

  • REST Endpoint: POST /wp-json/getgenie/v1/store/{post_id}/{key}
  • Vulnerable Parameter: post_id (URL path), key (URL path), and the Request Body (JSON).
  • Authentication: Authenticated, Author-level access (publish_posts capability).
  • Nonce: Requires a standard WordPress REST API nonce (wp_rest).
  • Target Key: serpData (allowed via getgenie_blogwizard_store_objects()).

3. Code Flow

  1. Route Registration: app/Api/Store.php registers the route getgenie/v1/store/(?P<post_id>[\d]+)/(?P<key>[\w-]+) with permission_callback set to __return_true.
  2. Authorization Check: Inside action($request):
    • Line 31: Verifies wp_rest nonce.
    • Line 38: Checks current_user_can('publish_posts'). This is the only permission check.
  3. IDOR/Sink:
    • Line 46: Extracts $post_id directly from the request.
    • Line 50: Checks if $key is in the allowed list returned by getgenie_blogwizard_store_objects().
    • Line 76: Calls update_post_meta($post_id, $prefix . $key, wp_slash($data)) without checking if the current user is the author of $post_id or an administrator.
  4. XSS Trigger: When the GetGenie sidebar is loaded in the Gutenberg editor, genie_header_script_data() (in getgenie.php) retrieves the meta:
    • Line 148: Loops through getgenie_blogwizard_store_objects().
    • Line 151: Fetches meta using get_post_meta(get_the_ID(), GETGENIE_BLOGWIZARD_PREFIX . $object, true).
    • The data is localized into the config JS object and rendered in the React/Vue sidebar without proper output escaping.

4. Nonce Acquisition Strategy

The plugin localizes a configuration object containing the REST nonce for the WordPress editor.

  1. Identify Script Localization: The function genie_header_script_data creates a $config array.
  2. Access Method:
    • Create a post as the Author user.
    • Navigate to the Edit Post page in the browser.
    • The nonce is stored in a global JavaScript variable. Based on line 171 of getgenie.php, the variable is likely named getgenie_config or similar (this is the standard pattern for this plugin).
  3. Execution Agent Steps:
    • wp post create --post_type=post --post_status=publish --post_author=author_id --post_title='Nonce Page'
    • Use browser_navigate to the edit URL of the new post.
    • browser_eval("window.getgenie_config?.restNonce") to extract the wp_rest nonce.

5. Exploitation Strategy

Step 1: Data Preparation

Identify the post_id of a post owned by an Administrator (usually ID=1).

Step 2: Craft Payload

The serpData logic (lines 58-74) expects a JSON structure. To trigger the "Competitor" tab XSS, we target the competitorData array.
Payload:

{
  "competitorData": [
    {
      "url": "http://example.com",
      "title": "Malicious Site",
      "content": "Test content",
      "some_rendered_field": "<img src=x onerror=alert('CVE-2026-2257_XSS')>"
    }
  ]
}

Step 3: Send Attack Request

Perform a POST request to the store endpoint targeting the Admin's post.

  • Method: POST
  • URL: /wp-json/getgenie/v1/store/1/serpData (assuming Admin post ID is 1)
  • Headers:
    • X-WP-Nonce: [EXTRACTED_NONCE]
    • Content-Type: application/json
  • Body: The JSON payload from Step 2.

6. Test Data Setup

  1. Target User: Create an Administrator user (admin_user).
  2. Target Post: Create a post as admin_user with ID=1.
  3. Attacker User: Create a user with the Author role (attacker_author).
  4. Environment: Ensure GetGenie <= 4.3.2 is active.

7. Expected Results

  • The REST API should return a 200 OK status with a JSON response: {"status": "success", "message": ["Successfully stored"], ...}.
  • The database entry for post_id=1 with meta key getgenie_blogwizard_serpData will now contain the XSS payload.
  • When an administrator edits Post 1 and opens the GetGenie sidebar (specifically the Competitor Analysis section), the alert will trigger.

8. Verification Steps

After the HTTP request, verify the injection using WP-CLI:

wp post meta get 1 getgenie_blogwizard_serpData

Expected output should contain: <img src=x onerror=alert('CVE-2026-2257_XSS')>

9. Alternative Approaches

If the serpData key is not processed as expected, try other keys in getgenie_blogwizard_store_objects() that are rendered in the UI:

  • generatedTitles
  • generatedIntros
  • selectedTitle

If the nonce is not found in window.getgenie_config, check window.wpApiSettings.nonce which is the default WordPress REST nonce location often used as a fallback.

Research Findings
Static analysis — not yet PoC-verified

Summary

The GetGenie plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) via its REST API, allowing authenticated users with Author-level permissions and above to modify metadata for arbitrary posts. Because the plugin also fails to sanitize this user-supplied metadata, attackers can inject malicious scripts that execute in the session of a higher-privileged user (like an Administrator) when they view the affected post's Competitor Analysis tab in the plugin's sidebar, resulting in Stored Cross-Site Scripting (XSS).

Vulnerable Code

// app/Api/Store.php:21
        if (!is_user_logged_in() || !current_user_can('publish_posts')) {
            return [
                'status'  => 'fail',
                'message' => ['Access denied.'],
            ];
        }

        $data = $request->get_body();

        $post_id = $request['post_id'];

---

// app/Api/Store.php:77
        update_post_meta($post_id, $prefix . $key, wp_slash($data));

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.2/app/Api/GetGenieChat.php /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.3/app/Api/GetGenieChat.php
--- /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.2/app/Api/GetGenieChat.php	2026-01-19 09:36:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.3/app/Api/GetGenieChat.php	2026-03-11 08:44:08.000000000 +0000
@@ -75,6 +75,15 @@
             $conversation_id = wp_insert_post($record);
             $message = 'Chat created successfully.';
         } else {
+            // Verify the post exists, belongs to current user, and is the correct post type
+            $post = get_post($conversation_id);
+            if (!$post || $post->post_type !== 'getgenie_chat' || (int) $post->post_author !== get_current_user_id()) {
+                return [
+                    'status'  => 'fail',
+                    'message' => ['Access denied. You can only update your own chat conversations.'],
+                ];
+            }
+
             $record = array(
                 'ID'          => $conversation_id,
                 'post_title'  => $req->templateSlug . '-' . date('Y-m-d H:i:s'),
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.2/app/Api/Store.php /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.3/app/Api/Store.php
--- /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.2/app/Api/Store.php	2023-05-08 07:08:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/getgenie/4.3.3/app/Api/Store.php	2026-03-11 08:44:08.000000000 +0000
@@ -29,7 +29,9 @@
             ];
         }
 
-        if (!is_user_logged_in() || !current_user_can('publish_posts')) {
+        $post_id = $request['post_id'];
+
+        if (!is_user_logged_in() || !current_user_can('edit_post', $post_id)) {
             return [
                 'status'  => 'fail',
                 'message' => ['Access denied.'],
@@ -37,8 +39,6 @@
         }
 
         $data = $request->get_body();
-
-$post_id = $request['post_id'];
         $key     = $request['key'];
         $prefix  = GETGENIE_BLOGWIZARD_PREFIX;
 
@@ -71,6 +71,9 @@
 
         }
 
+        // Sanitize the data to prevent XSS attacks
+        $data = wp_kses_post($data);
+
         update_post_meta($post_id, $prefix . $key, wp_slash($data));
 
         return [

Exploit Outline

1. Authenticate to the target WordPress site as a user with at least the 'Author' role. 2. Extract a valid WordPress REST API nonce from the page source of the block editor (typically found in the `getgenie_config` JavaScript object or `wpApiSettings`). 3. Identify the `post_id` of a target post owned by an Administrator. 4. Craft a JSON payload containing a malicious script within the `competitorData` array, such as: `{"competitorData": [{"url": "http://example.com", "title": "XSS", "content": "test", "some_rendered_field": "<img src=x onerror=alert(document.cookie)>"}]}`. 5. Send a POST request to `/wp-json/getgenie/v1/store/{target_post_id}/serpData` with the `X-WP-Nonce` header set to the extracted nonce and the JSON payload in the body. 6. The XSS will trigger when an Administrator edits the target post and opens the GetGenie sidebar's 'Competitor' section.

Check if your site is affected.

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