A Guest Writes Your Error Log, the AI Reads It, the Admin Executes It — CVE-2026-96561 in AI Engine
CVE-2026-96561 reached NVD on 1 October 2026 with a CVSS 3.1 score of 7.2 High (AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N), published by Wordfence and credited to whale120 of National Taiwan University. It affects AI Engine – The Chatbot, AI Framework & MCP for WordPress through version 3.8.0, a plugin with roughly 90,000 active installations. The fix is 3.8.1, released 24 September 2026; current is 3.8.3.
The CWE on the record is CWE-79, stored cross-site scripting. That label is correct and badly undersells what happened. Four separate components each behaved reasonably on their own, and the composition of them lets an unauthenticated visitor put JavaScript into a WordPress administrator's dashboard — by way of the site's own AI assistant.
Four links, none of them a vulnerability alone
We read the plugin source at tags 3.8.0 and 3.8.1 from the WordPress plugin SVN to confirm each step.
Link one: a denylist that canonicalises after it filters. The public REST endpoint /mwai-ui/v1/chats/submit strips server-controlled parameters from a visitor's request body — model, envId, instructions, apiKey and friends — unless the caller can configure chatbots anyway. In 3.8.0 the strip is an exact-key unset() over MWAI_CHATBOT_SERVER_PARAMS. Later, convert_keys() walks each key and converts snake_case to camelCase by deleting underscores and capitalising what follows. The key model_ survives the denylist, because it is not model — and then convert_keys() turns it back into model. The filter and the normaliser disagree about what a parameter name is, and the request is processed in the order that loses.
Link two: an exception message as a log-write primitive. The attacker-controlled string now sits in $query->model. Validation rejects it, throwing an exception whose message embeds the raw value. On the non-streaming, non-admin path that message is written to the PHP error log unmodified — including any CR/LF the attacker put in it. A newline in a log file means the attacker is no longer writing a field; they are writing lines. They can forge a complete, plausible log entry of their own design.
Link three: the log becomes an LLM prompt. AI Engine's Advisor module assembles a daily prompt describing the site — installed plugins, versions, PHP version, WordPress version, theme, site URL — and then appends the last ten entries from MeowKit_MWAI_Helpers::php_error_logs(), verbatim. The forged lines are now instructions sitting inside a prompt the site pays a frontier model to answer. This is textbook indirect prompt injection, and Wordfence tags it as such with CWE-1427. It is also the LogJack pattern we covered in April — log sinks are an underrated injection channel precisely because everyone classifies them as output.
Link four: the model's answer is printed unescaped. The returned JSON is stored in the mwai_advisor_data option with no schema validation and no HTML sanitisation. advisor_metabox() then concatenates it into the dashboard widget. The 3.8.0 source is unambiguous:
echo '<strong>' . $title . '</strong> - ' . $description;
No esc_html(), no wp_kses(). The string the model was persuaded to emit renders as markup the moment an administrator loads wp-admin.
Why the scope flag matters more than the score
7.2 is a mid-table number, and the impact sub-scores are only C:L/I:L. The field that carries the weight is S:C — scope changed — together with PR:N and UI:N. The attacker needs no account and no interaction with the victim beyond the administrator doing the one thing administrators do daily. Script executing in an authenticated admin session on a WordPress install is the start of every full-takeover chain: nonce theft, a new administrator, a plugin upload. We traced that exact escalation last week in Comment2XSS, and the shape here is identical once the payload lands.
What is different is the delivery vehicle. Stored XSS normally requires the application to persist attacker bytes and replay them. Here the attacker persists nothing directly: they write to a log, and the application's own AI feature volunteers to carry the payload across the trust boundary, laundering it through a model response that the dashboard treats as first-party content. The plugin trusted the model's output because it trusted its own prompt — and it never asked who had been writing to that prompt.
That is the generalisable lesson, and it is not WordPress-specific. Any pipeline that feeds operational telemetry to an LLM and renders the result in a privileged UI has the same three-part exposure: an untrusted write into the telemetry, no provenance boundary inside the prompt, and no output encoding on the way back. Most teams building AI "insights" widgets this year have all three.
What the fix actually changed
3.8.1 closes two links rather than one, which is the right call for a chain like this.
The parameter-name mismatch is fixed at its root. convert_keys() now delegates to a new canonical_key() helper, and a new client_params() method canonicalises first and then drops denied keys — with a comment in the source stating plainly that doing it in that order "is the fix for CVE-2026-96561." The denylist also moved into a declared constant, CLIENT_DENIED_PARAMS, naming 30-odd settings a visitor must never set: model, envId, apiKey, instructions, mcpServers, tools, promptId and so on. Matching is case-insensitive on the canonical form.
The output is fixed too. advisor_metabox() now coerces title and description through is_scalar() and wraps both in esc_html(), with a comment that states the threat model in one sentence: "Model output: its prompt includes PHP error log lines, which visitors can influence." That is a better sentence than most advisories manage.
The plugin's own changelog, by contrast, describes 3.8.1 as "Security: visitors could override a chatbot's or form's model, instructions or environment with renamed parameters. Fixed, and the Advisor widget now escapes its output" — accurate, but third in a list that leads with a new chatbot theme called Glass. The release shipped on 24 September; Wordfence published on 30 September; the CVE appeared on 1 October. A site on auto-updates was patched before the public record existed, which is the system working — but it also means the only contemporaneous signal was a changelog line sitting under a UI feature.
What to do
- Update AI Engine to 3.8.1 or later. 3.8.3 (1 October 2026) is current. Both halves of the fix are in 3.8.1; there is no partial mitigation that substitutes for it.
- If you cannot update immediately, disable the Advisor module.
run_advisor()returns early whenmodule_advisoris off, which severs links three and four — the forged log line is written but never reaches a prompt or the dashboard. The parameter-override bug remains, so treat this as a holding action, not a fix. - Check whether
mwai_advisor_dataalready holds a payload. The option persists model output across requests; an administrator who has not yet loaded the dashboard since an attempted injection is still exposed. Inspecting and clearing that option is cheap. - Audit your own prompt builders for log and telemetry inputs. Error logs, access logs, support tickets, webhook bodies, queue messages. If an unauthenticated user can influence any byte that reaches a prompt, the model's response is untrusted input and must be encoded on output — and preferably schema-validated before storage. Neither control was present here; either one alone would have stopped the chain.
- Escape model output in privileged UIs, always. This is the single highest-leverage habit for the current wave of AI dashboard features. The model is not a trusted author; it is a very persuasive reflector of whatever reached its context window.
Our verification was static: we read classes/modules/advisor.php, classes/modules/chatbot.php and classes/query/base.php at the published 3.8.0 and 3.8.1 tags in the WordPress plugin SVN, and compared them. We did not run the plugin, submit any request to a live site, or attempt the injection.
Sources:
- NVD — CVE-2026-96561 (published 1 October 2026; CVSS 3.1 7.2 High from Wordfence; CWE-79; affected AI Engine <= 3.8.0; full chain description quoted in part above)
- Wordfence Intelligence — "AI Engine <= 3.8.0 – Unauthenticated Stored Cross-Site Scripting via 'model_' Parameter → PHP Error-Log Injection → Advisor Indirect Prompt Injection" (publicly published 30 September 2026; researcher whale120, National Taiwan University; patched version 3.8.1)
- AI Engine 3.8.0 — classes/modules/advisor.php (unescaped advisor_metabox() output and run_advisor() appending php_error_logs() to the prompt)
- AI Engine 3.8.1 — classes/query/base.php (canonical_key(), CLIENT_DENIED_PARAMS and client_params(); in-source comment naming CVE-2026-96561)
- WordPress.org plugin directory — AI Engine (90,000 active installations; 3.8.1 released 24 September 2026; 3.8.3 released 1 October 2026; changelog text quoted above)