Special:Ask accepts a cursor query parameter for keyset pagination (added in 7.0.0). The token is decoded by CursorEncoder, which is an unsigned base64url-encoded JSON blob, so its contents are fully attacker-controlled. When the cursor's sort anchor does not match the request's sort= / order=, QueryCreator::applyCursorIfRequested() builds an error message by interpolating the attacker-controlled sort_prop / sort_order values into a raw string via Query::addErrors().
Unlike SMW's message-key errors, this raw string bypasses the message layer's sanitisation (ProcessingErrorMsgHandler::normalizeAndDecodeMessages() passes a non-key, non-encoded string through unchanged). ErrorWidget::queryError() then assembles the errors and passes them to Html::errorBox(), whose first argument is emitted as raw HTML. The result is a reflected cross-site scripting vulnerability: a crafted cursor token containing markup in sort_prop (or sort_order) executes script in the victim's browser on the wiki origin.
No authentication or special permission is required. The payload is delivered via a crafted link, e.g. Special:Ask?q=...&p[cursor]=<forged token> where the token decodes to {"v":1,"sort_prop":"<script>...</script>"}. Confirmed executing in a browser; the responses carry no Content-Security-Policy, so inline script is not blocked.
sort_prop / sort_order values before they are interpolated into the error text in QueryCreator, so no raw user input enters the error stream.format=debug output path that reflected the same error text was hardened separately in GHSA-q5fm-9mx6-44f4.Query error strings are rendered as raw HTML by ErrorWidget::queryError() via Html::errorBox(). Any error added through Query::addErrors() that embeds user-controlled text must be output-encoded (or use a message key); pagination and validation error text is attacker-influenced and must not be treated as trusted.
{
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-18T16:58:50Z",
"nvd_published_at": null,
"severity": "MODERATE"
}