Semantic MediaWiki server-side boundary wave: unauthenticated admin API, unsigned cursor tokens, and debug-output XSS sinks (GHSA, 2026-09-18)¶
Sources: GitHub Security Advisories published 2026-09-18T16:40–16:59Z (one 10-advisory Semantic MediaWiki wave; the stored data-subtab XSS from the same product family is covered on the media client-side render sinks page).
- GHSA-jr78-w6w5-m8f8:
api.php?action=smwtaskhas no authorization check — unauthenticated admin-only maintenance tasks - GHSA-cx86-7xwp-w9wf / CVE-2026-77616:
Special:Askreflected XSS via forged unsigned cursor pagination token - GHSA-q5fm-9mx6-44f4 / CVE-2026-77610:
format=debug/DebugFormatterreflected XSS (raw SQL + EXPLAIN echo) - GHSA-9rcc-pmj8-ffhr:
Special:FacetedSearchcstatereflected XSS gated only bycrc32(q)checksum - GHSA-hw3m-8j5x-94ff / CVE-2026-77609:
Special:URIResolveropen redirect via interwiki-prefixed subpage - GHSA-7xv3-gf2g-498h / CVE-2026-77607:
Special:Asktablesepseparator reflected XSS (also viarequest_type=raw) - GHSA-59xw-qv23-j3rc / CVE-2026-77608:
Special:SearchByPropertyproperty/valuereflected XSS in form + error text - GHSA-3jp5-3h47-28qf / CVE-2026-77606:
Special:Askheaders=plainheader reflection
The durable lesson: every alternate programmatic surface (API module, debug format, raw output mode, pagination token, checksum-gated state) is a separate trust boundary that must be tested even when the human-facing feature is locked down or escaped.
Why this is worth an operator pattern¶
- API module vs Special page authorization parity.
api.php?action=smwtaskruns admin-only maintenance tasks (table-statistics,duplicate-lookup,insert-job,update,check-query,run-joblist) while the equivalentSpecial:SMWAdminUI requiressmw-admin. The module's only gates wereneedsToken('csrf')+mustBePosted()— and MediaWiki hands anonymous users the fixed public CSRF token+\, so a token check is not an authorization check. Reusable sweep: for every privileged Special page or admin UI on any platform, enumerateapi.php?action=*(or the REST equivalent) for modules that back the same operations and diff the permission gates; run the read tasks anonymously as a state oracle and note thatinsert-job+smw.entityIdDisposerwith a chosenidturns disclosure into targeted mutation of stored data. - Unsigned "opaque" tokens are attacker-controlled input. The
Special:Askcursor is base64url JSON with no signature — decode, editsort_prop/sort_order, re-encode. When the anchor mismatches the requestsort=, the error text interpolates those values into a raw string that bypasses the message-layer sanitizer and lands inHtml::errorBox()raw. Rule: any pagination/state/cursor/token that decodes to readable JSON/JWT-without-verification is a reflected-XSS candidate at every sink that echoes its fields into error paths. - Checksum gates are compute-and-bypass controls.
Special:FacetedSearchonly preservescstatewhencsum == crc32(q)(orfiltered=1). Both are trivially computable client-side (crc32of your ownq), so the "gate" filters nobody. Whenever a request parameter survives only if it satisfies a deterministic function of other attacker-controlled parameters, compute the function and pass the gate. - Debug/verbose output paths are forgotten XSS sinks.
format=debugechoes the re-serialized query (escaping only[), the generated SQL verbatim (SQL-quoted ≠ HTML-escaped), andEXPLAINplan text containing the value literals — as rawaddHTML, never through the parser sanitizer. On MediaWiki-family and any query UI: requestformat=debug/debug=1/trace/verbose variants with an inert markup canary in a text-typed property value, on both the special page and any inline-parser context (the inline{{#ask|format=debug}}leg was safe here because output re-enters the sanitizer — the special-page leg was not). - Raw/alternate output modes double the sink count. The
sepinjection was reachable through standard render andrequest_type=raw; test every output-format parameter variant (format=raw/json/csv/debug) as its own render pipeline. - Redirect validation belongs at the sink.
Special:URIResolver303-redirected to a title resolved from a user-controlled subpage without host checks; an interwiki prefix (Special:URIResolver/mw:Foo) hops off-host from a trusted wiki URL. Always re-validate the final target URL (host + no userinfo component), not the input path — and sweep interwiki/alias-prefix resolution as an off-host channel.
Validation workflow (authorized scope only)¶
Lab wiki, anonymous read-only proofs, inert canaries
Run against a lab or explicitly authorized Semantic MediaWiki / MediaWiki target. All proofs below are anonymous or low-privilege; use inert DOM markers (<img src=x onerror="window.__canary=1">-style) and no network callbacks.
- API parity sweep: enumerate
api.php?action=*modules (action=query&meta=siteinfosiprop=apimodules) and fingerprint SMW (action=smwtask,smwprops, etc.). For each module backing an admin UI feature, replay it anonymously with the public token+`:curl -s 'HOST/api.php?action=query&meta=tokens&type=csrf&format=json'then POSTaction=smwtask&task=table-statistics&token=+\. HTTP 200 with store statistics = authorization-parity finding; record the task list you reached. Do not runinsert-job/run-joblistbeyond a single benign enqueue in a lab, and never against production wikis. - Cursor-token XSS: fetch a normal
Special:Askpagination cursor, base64url-decode it, setsort_propto an inert structural marker, re-encode, submit with mismatchedsort=; diff submitted → served bytes → parsed DOM (no CSP means inline markup survives here). - Debug sink:
Special:Ask?q=[[Text::<inert-marker>]]&debug=1as anonymous; check all three echo legs (query string, prettified SQL, EXPLAIN) — escaping one sink is not escaping the path. - Checksum gate: pick
q, computecsum=crc32(q)in any language (or just sendfiltered=1), inject an inert marker intocstate[0], confirm raw mustache{{{hidden}}}output. - Redirect + separator controls: resolve
Special:URIResolver/<interwiki>:Fooand observe 303 off-host with a user-info-bearing target rejected post-fix; re-request a table query withsepset to a marker and withrequest_type=raw. - Version control: patched release must return 403/permission errors for smwtask and encode all marker legs.
Reporting checklist¶
- State the anonymous-token detail explicitly (
+\fixed public CSRF token) — it preempts the "but a token was required" triage dismissal. - Separate the classes: missing authorization (smwtask, CVSS 7.3, state-changing), reflected XSS (cursor, debug, cstate, sep, headers, SearchByProperty — note absence of CSP), open redirect (URIResolver).
- For smwtask, name the reachable tasks and the disclosure→mutation bridge (object-ID enumeration via
duplicate-lookupfeedingentityIdDisposerid parameter). - Cite the sink chain (
Query::addErrors()→ErrorWidget::queryError()→Html::errorBox()raw) — error-path text is attacker-influenced and must not be treated as trusted.
Related pages: Semantic MediaWiki stored data-* client-side sink (same product, opposite half: front-end dataset sink), the Sept 18 Nextcloud app-layer authority drift (REST-vs-alternate-transport authorization parity is the same axis), and the Sept 16 sanitizer-gap page.