Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/ai-engine/ai-engine.php
+++ b/ai-engine/ai-engine.php
@@ -4,7 +4,7 @@
Plugin Name: AI Engine
Plugin URI: https://wordpress.org/plugins/ai-engine/
Description: AI meets WordPress. Your site can now chat, write poetry, solve problems, and maybe make you coffee.
-Version: 3.4.9
+Version: 3.5.0
Author: Jordy Meow
Author URI: https://jordymeow.com
Text Domain: ai-engine
@@ -12,7 +12,7 @@
License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/
-define( 'MWAI_VERSION', '3.4.9' );
+define( 'MWAI_VERSION', '3.5.0' );
define( 'MWAI_PREFIX', 'mwai' );
define( 'MWAI_DOMAIN', 'ai-engine' );
define( 'MWAI_ENTRY', __FILE__ );
--- a/ai-engine/classes/engines/chatml.php
+++ b/ai-engine/classes/engines/chatml.php
@@ -1528,6 +1528,7 @@
return preg_replace( '/-d{4}-d{2}-d{2}$/', '', $model );
}
+ // TODO: Remove every list_finetunes / cancel_finetune / delete_finetune / run_finetune method below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06). The `finetunes` env keys in init.php and the `finetune` key on every entry in constants/models.php go with them.
public function list_deleted_finetunes( $envId = null, $legacy = false ) {
$finetunes = $this->list_finetunes( $legacy );
$deleted = [];
@@ -2010,6 +2011,7 @@
public function get_models() {
$models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
+ // TODO: Drop the fine-tune injection loop below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
$finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
foreach ( $finetunes as $finetune ) {
if ( empty( $finetune['status'] ) || $finetune['status'] !== 'succeeded' ) {
@@ -2062,6 +2064,7 @@
}
}
+ // TODO: After 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06), drop the $finetune branch from calculate_price() and the finetune-detection block in get_price().
private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false ) {
$modelFamily = self::get_model_without_release_date( $modelFamily );
$models = $this->get_models();
--- a/ai-engine/classes/engines/core.php
+++ b/ai-engine/classes/engines/core.php
@@ -445,7 +445,7 @@
// Check if the model is available, except if it's an assistant
if ( !( $query instanceof Meow_MWAI_Query_Assistant ) ) {
- // TODO: Avoid checking on the finetuned models for now.
+ // TODO: Remove the ft: bypass after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06). Until then we skip model validation for fine-tunes so user-created models still resolve.
if ( substr( $query->model, 0, 3 ) === 'ft:' ) {
return;
}
--- a/ai-engine/classes/rest.php
+++ b/ai-engine/classes/rest.php
@@ -352,6 +352,7 @@
'permission_callback' => [ $this->core, 'can_access_settings' ],
'callback' => [ $this, 'rest_openai_files_download' ],
] );
+ // TODO: Remove all the /openai/finetunes/* and /openai/files/finetune routes after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
register_rest_route( $this->namespace, '/openai/files/finetune', [
'methods' => 'POST',
'permission_callback' => [ $this->core, 'can_access_settings' ],
@@ -1183,6 +1184,7 @@
}
}
+ // TODO: Remove all the rest_openai_*finetune* handlers below after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
public function rest_openai_deleted_finetunes_get() {
try {
$envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
--- a/ai-engine/constants/init.php
+++ b/ai-engine/constants/init.php
@@ -125,6 +125,7 @@
'module_moderation' => false,
'module_statistics' => false,
'statistics_retention_days' => 'Never',
+ // TODO: Remove module_finetunes after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
'module_finetunes' => false,
'module_embeddings' => false,
'module_transcription' => false,
@@ -167,6 +168,7 @@
'name' => 'OpenAI',
'type' => 'openai',
'apikey' => '',
+ // TODO: Drop the four finetunes* keys after 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06).
'finetunes' => [],
'finetunes_deleted' => [],
'legacy_finetunes' => [],
--- a/ai-engine/constants/models.php
+++ b/ai-engine/constants/models.php
@@ -2,6 +2,8 @@
// Price as of June 2024: https://openai.com/api/pricing/
+// TODO: After 2027-02 (OpenAI ends fine-tune job creation on 2027-01-06), drop the `finetune` key from every model entry below and remove 'finetune' from every model's `tags` array.
+
define( 'MWAI_OPENAI_MODELS', [
/*
GPT-5.5
@@ -137,6 +139,7 @@
GPT-5.3 Chat
GPT-5.3 model used in ChatGPT
https://developers.openai.com/api/docs/models/gpt-5.3-chat-latest
+ Shutdown: August 10, 2026.
*/
[
'model' => 'gpt-5.3-chat-latest',
@@ -152,7 +155,7 @@
'maxCompletionTokens' => 16384,
'maxContextualTokens' => 128000,
'finetune' => false,
- 'tags' => ['core', 'chat', 'vision', 'files', 'functions', 'json', 'responses', 'mcp'],
+ 'tags' => ['core', 'chat', 'vision', 'files', 'functions', 'json', 'responses', 'mcp', 'deprecated'],
'tools' => ['image_generation', 'code_interpreter']
],
/*
@@ -185,6 +188,7 @@
GPT-5.2 Chat
GPT-5.2 model used in ChatGPT
https://platform.openai.com/docs/models/gpt-5.2
+ Shutdown: August 10, 2026.
*/
[
'model' => 'gpt-5.2-chat-latest',
@@ -200,7 +204,7 @@
'maxCompletionTokens' => 128000,
'maxContextualTokens' => 400000,
'finetune' => false,
- 'tags' => ['core', 'chat', 'vision', 'files', 'responses', 'mcp'],
+ 'tags' => ['core', 'chat', 'vision', 'files', 'responses', 'mcp', 'deprecated'],
'tools' => ['web_search', 'file_search', 'code_interpreter']
],
/*
--- a/ai-engine/labs/mcp-oauth.php
+++ b/ai-engine/labs/mcp-oauth.php
@@ -17,12 +17,11 @@
* like Claude Desktop that drive the user through a browser authorize flow.
*/
class Meow_MWAI_Labs_MCP_OAuth {
-
- const DB_VERSION = '1.0.0';
- const ACCESS_TOKEN_TTL = 3600; // 1 hour
- const REFRESH_TOKEN_TTL = 2592000; // 30 days
- const AUTH_CODE_TTL = 60; // seconds
- const NONCE_ACTION = 'mwai_mcp_oauth_consent';
+ public const DB_VERSION = '1.0.0';
+ public const ACCESS_TOKEN_TTL = 3600; // 1 hour
+ public const REFRESH_TOKEN_TTL = 2592000; // 30 days
+ public const AUTH_CODE_TTL = 60; // seconds
+ public const NONCE_ACTION = 'mwai_mcp_oauth_consent';
private $core;
private $mcp;
@@ -384,6 +383,17 @@
}
$user = wp_get_current_user();
+ // Capability gate. MCP grants administrative tool access by design; allowing a
+ // non-admin to mint an OAuth token would let them act through the MCP layer with
+ // privileges they do not hold in WordPress itself.
+ if ( !$this->user_can_authorize( $user->ID ) ) {
+ if ( $this->logging ) {
+ error_log( '[AI Engine MCP OAuth] ❌ Non-admin user ' . $user->ID . ' tried to authorize client ' . $params['client_id'] );
+ }
+ $this->render_error_page( 'Only administrators can authorize MCP applications on this site.' );
+ exit;
+ }
+
$this->render_consent_page( $client, $params, $user );
exit;
}
@@ -394,6 +404,14 @@
exit;
}
+ if ( !$this->user_can_authorize( get_current_user_id() ) ) {
+ if ( $this->logging ) {
+ error_log( '[AI Engine MCP OAuth] ❌ Non-admin user ' . get_current_user_id() . ' attempted authorize submit' );
+ }
+ $this->render_error_page( 'Only administrators can authorize MCP applications on this site.' );
+ exit;
+ }
+
$nonce = (string) $request->get_param( '_mwai_nonce' );
if ( !wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
$this->render_error_page( 'Security check failed. Please try again from your application.' );
@@ -614,12 +632,29 @@
$hash = hash( 'sha256', $token );
$wpdb->query( $wpdb->prepare(
"UPDATE {$this->table_tokens} SET revoked = 1 WHERE access_token_hash = %s OR refresh_token_hash = %s",
- $hash, $hash
+ $hash,
+ $hash
) );
return new WP_REST_Response( null, 200 );
}
#endregion
+ #region Capability gate
+ /**
+ * Whether a user is allowed to authorize an OAuth client and to use an OAuth
+ * access token against the MCP endpoint. Defaults to administrator only,
+ * matching the documented MCP access model. The filter exists so the planned
+ * multi-user MCP work can broaden this safely once per-token capability
+ * scoping lands; until then, allowing a non-admin here re-opens CVE-class
+ * privilege escalation through tools like wp_create_user.
+ */
+ public function user_can_authorize( $user_id ) {
+ $user_id = (int) $user_id;
+ $allowed = $user_id > 0 && user_can( $user_id, 'administrator' );
+ return (bool) apply_filters( 'mwai_mcp_oauth_user_can_authorize', $allowed, $user_id );
+ }
+ #endregion
+
#region Token validation (called from MCP auth path)
/**
* Validate an access token for protected resource access.
--- a/ai-engine/labs/mcp.php
+++ b/ai-engine/labs/mcp.php
@@ -204,6 +204,16 @@
if ( $this->oauth ) {
$token_data = $this->oauth->validate_token( $token );
if ( $token_data ) {
+ // Defense in depth: even if a token was issued (or stored from before
+ // the authorize-time admin gate landed), only accept it if the linked
+ // user still holds administrator capability. Otherwise a Subscriber's
+ // OAuth token would inherit the global mcp_role and reach admin tools.
+ if ( !$this->oauth->user_can_authorize( $token_data['user_id'] ) ) {
+ if ( $this->logging ) {
+ error_log( '[AI Engine MCP] ❌ OAuth token rejected: user ' . $token_data['user_id'] . ' is not an administrator.' );
+ }
+ return false;
+ }
// Set current user based on OAuth token
wp_set_current_user( $token_data['user_id'] );
$auth_result = 'oauth';