CVE-2026-1900

Link Whisper Free < 0.9.1 - Missing Authorization to Unauthenticated Settings Change

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
0.9.1
Patched in
9d
Time to patch

Description

The Link Whisper Free plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to 0.9.1 (exclusive). This makes it possible for unauthenticated attackers to perform an unauthorized action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<0.9.1
PublishedApril 7, 2026
Last updatedApril 15, 2026
Affected pluginlink-whisper

What Changed in the Fix

Changes introduced in v0.9.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1900 - Link Whisper Free Missing Authorization ## 1. Vulnerability Summary The **Link Whisper Free** plugin for WordPress is vulnerable to an unauthenticated settings change and user meta modification due to missing authorization checks in its REST API impleme…

Show full research plan

Exploitation Research Plan: CVE-2026-1900 - Link Whisper Free Missing Authorization

1. Vulnerability Summary

The Link Whisper Free plugin for WordPress is vulnerable to an unauthenticated settings change and user meta modification due to missing authorization checks in its REST API implementation.

Specifically, the Wpil_Rest class registers several REST API endpoints with the permission_callback set to __return_true. While one endpoint (site_interlinking_handler) immediately calls die(), the ai_auth_handler and handler_rest functions process user-supplied input and update global WordPress options and user metadata without verifying the requester's identity or capabilities.

The most critical sink is in Wpil_Rest::ai_auth_handler, which allows an unauthenticated attacker to:

  1. Overwrite the plugin's AI access token and associated user IDs/emails.
  2. Modify the wpil_ai_access_user_id and wpil_ai_access_user_email metadata for any arbitrary user ID on the system.
  3. Force the plugin into an "authorized" state for AI features.

2. Attack Vector Analysis

  • Endpoint: /wp-json/link-whisper/ai-auth (defined by Wpil_Rest::REST_SLUG and Wpil_Rest::AI_AUTH)
  • Method: POST
  • Capability Required: None (unauthenticated)
  • Nonce Required: None
  • Payload Parameters:
    • access_token: String (must contain the substring ai-)
    • user_id: String (attacker-controlled value)
    • uid: Integer (target user ID to modify metadata for)
    • uemail: String (attacker-controlled email)

3. Code Flow

  1. Entry Point: During rest_api_init, Wpil_Rest::register_rest() is called.
  2. Route Registration: The route link-whisper/ai-auth is registered with 'permission_callback' => "__return_true".
  3. Callback Execution: When a POST request is made to this route, Wpil_Rest::ai_auth_handler() is invoked.
  4. Logic Branch: The function checks if the access_token parameter is present and contains the string ai- (via false !== strpos($token, 'ai-')).
  5. Sinks:
    • update_option('wpil_ai_access_token', ...)
    • update_option('wpil_ai_access_user_id', $user_id)
    • update_option('wpil_ai_access_user_email', $uemail)
    • update_user_meta($uid, 'wpil_ai_access_user_id', $user_id)
    • update_user_meta($uid, 'wpil_ai_access_user_email', $uemail)
    • update_option('wpil_ai_access_authorized', true)

4. Nonce Acquisition Strategy

This vulnerability does not require a nonce. The register_rest_route call specifically uses permission_callback => "__return_true", and the callback function ai_auth_handler does not perform any wp_verify_nonce or check_ajax_referer checks.

5. Exploitation Strategy

Step-by-Step Plan

  1. Target Selection: Identify a target user ID (usually 1 for the primary administrator).
  2. Craft Payload: Create a POST request that satisfies the strpos($token, 'ai-') check and provides target values.
  3. Execute Request: Submit the request to the REST API.
  4. Verification: Check if the options and user meta have been updated.

HTTP Request (using http_request tool)

{
  "method": "POST",
  "url": "http://vulnerable-wp.local/wp-json/link-whisper/ai-auth",
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "body": "access_token=ai-pwned-token&user_id=attacker_id_123&uid=1&uemail=hacker@evil.com"
}

6. Test Data Setup

  1. Install Link Whisper Free version 0.9.0.
  2. Ensure there is a user with ID 1 (standard WP setup).
  3. No specific plugin configuration is required as the vulnerability exists in the default registered routes.

7. Expected Results

  • Response Code: 200 OK
  • Response Body: "ok"
  • Side Effects:
    • Option wpil_ai_access_token will contain the encrypted version of ai-pwned-token.
    • Option wpil_ai_access_user_id will be attacker_id_123.
    • Option wpil_ai_access_user_email will be hacker@evil.com.
    • User meta wpil_ai_access_user_id for User 1 will be attacker_id_123.
    • User meta wpil_ai_access_user_email for User 1 will be hacker@evil.com.
    • Option wpil_ai_access_authorized will be 1 (true).

8. Verification Steps

Use WP-CLI to verify the changes in the database:

# Check global options
wp option get wpil_ai_access_user_id
wp option get wpil_ai_access_user_email
wp option get wpil_ai_access_authorized

# Check user meta for the target user (UID 1)
wp usermeta get 1 wpil_ai_access_user_id
wp usermeta get 1 wpil_ai_access_user_email

9. Alternative Approaches

Exploiting handler_rest (Google Search Console)

The link-whisper/code endpoint is also unauthenticated.

  • Endpoint: /wp-json/link-whisper/code
  • Method: POST
  • Parameter: code=SOME_CODE
  • Impact: It calls Wpil_SearchConsole::get_access_token(trim($code)). If the code is accepted by the Search Console helper, it sets the wpil_gsc_app_authorized option to true. This is less direct than the AI auth exploit but still represents an unauthorized state change.

Data Injection via uemail

Since uemail is passed directly to update_option and update_user_meta without rigorous sanitization (other than WordPress's internal update_option sanitization), it can be used to inject arbitrary string data into the database that might be reflected in the admin dashboard (e.g., in the Settings templates like templates/wpil_settings_v2.php).

Research Findings
Static analysis — not yet PoC-verified

Summary

The Link Whisper Free plugin for WordPress is vulnerable to unauthorized settings modification and metadata injection due to missing capability checks on its REST API endpoints. Unauthenticated attackers can exploit the '/wp-json/link-whisper/ai-auth' route to overwrite the plugin's AI access configuration and modify the metadata of any user account, including administrators.

Vulnerable Code

// core/Wpil/Rest.php lines 66-74
            register_rest_route(self::REST_SLUG, self::AI_AUTH, [
                'methods'             => 'POST',
                'callback'            => [
                    $this,
                    'ai_auth_handler'
                ],
                'permission_callback' => "__return_true",
                'show_in_index'       => false
            ]);

---

// core/Wpil/Rest.php lines 131-155
    public function ai_auth_handler( WP_REST_Request $request )
    {
        if(!empty($request->get_param('access_token'))){
            $token = $request->get_param('access_token');
            $user_id = $request->get_param('user_id');
            $uid = (int)$request->get_param('uid');
            $uemail = $request->get_param('uemail');

            if(!empty($token) && false !== strpos($token, 'ai-')){
                // save the token to the options
                update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
                // and the user id
                update_option('wpil_ai_access_user_id', $user_id);
                // and the user email
                update_option('wpil_ai_access_user_email', $uemail);
                // tag the user with the id
                update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
                update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
                // and update the flag so we know it's live
                update_option('wpil_ai_access_authorized', true);
            }

            return 'ok';
        }

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/link-whisper/0.9.0/core/Wpil/Rest.php	2025-09-30 05:28:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/link-whisper/0.9.1/core/Wpil/Rest.php	2026-02-25 03:13:16.000000000 +0000
@@ -77,7 +77,7 @@
 
             if ( !empty($response['access_valid']) ) {
                 // and update the flag so we know it's live
-                update_option('wpil_gsc_app_authorized', true);
+                update_option('wpil_gsc_app_authorized', true, false);
             }
 
             return 'ok';
@@ -116,16 +116,20 @@
             $uid = (int)$request->get_param('uid');
             $uemail = $request->get_param('uemail');
 
-            if(!empty($token) && false !== strpos($token, 'ai-')){
+            if( !empty($token) && 
+                false !== strpos($token, 'ai-') && // if the code isn't corrupted
+                (bool) preg_match('/\\Aai-[0-9a-f]{64}\\z/i', $token) && // is a valid token
+                (bool) preg_match('/\\A[0-9a-f]{32}\\z/i', $user_id)) // has a valid id
+            {
                 // save the token to the options
                 update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
                 // and the user id
                 update_option('wpil_ai_access_user_id', $user_id);
                 // and the user email
-                update_option('wpil_ai_access_user_email', $uemail);
+                update_option('wpil_ai_access_user_email', sanitize_email($uemail));
                 // tag the user with the id
-                update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
-                update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
+//                update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
+//                update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
                 // and update the flag so we know it's live
                 update_option('wpil_ai_access_authorized', true);
             }

Exploit Outline

The exploit targets the `/wp-json/link-whisper/ai-auth` REST API route, which is registered with an unauthenticated `permission_callback` returning `true`. An attacker sends a POST request to this endpoint with a payload containing four parameters: `access_token` (which must start with 'ai-'), `user_id`, `uemail`, and `uid`. No WordPress nonces or login credentials are required. The server-side logic in `ai_auth_handler` directly uses these inputs to call `update_option()` and `update_user_meta()`. This allows the attacker to arbitrarily modify global AI credentials and overwrite user metadata (specifically `wpil_ai_access_user_id` and `wpil_ai_access_user_email`) for any target user ID provided in the `uid` parameter.

Check if your site is affected.

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