GHSA-9rcc-pmj8-ffhrMediumCVSS 6.1Disclosed before NVD

Semantic MediaWiki's Special:FacetedSearch cstate hidden inputs enable reflected XSS (residual of CVE-2025-10354)

Published
September 18, 2026
Last Modified
September 18, 2026

📋 Description

Summary

Special:FacetedSearch cstate hidden inputs enable reflected XSS (residual of CVE-2025-10354)

Details

Affected versions and vulnerable location

  • Confirmed present on latest shipped release tag available in the local clone: SemanticMediaWiki/[email protected].
  • Confirmed present on default branch master at HEAD 18f418b4cdf2875e67a741349179a22c1573f61c.

Vulnerable sink (default-branch representation):

  • src/MediaWiki/Specials/FacetedSearch/HtmlBuilder.php:131-133
    • Builds $hidden by concatenating unescaped request-controlled cstate[$key] values into an HTML attribute context (value="...").
  • templates/FacetedSearch/search.mustache:25
    • Inserts the constructed fragment via {{{hidden}}} (no HTML escaping at this boundary).

Reachability trace (verified from source)

  1. HTTP entrypoint:
    • GET to Special:FacetedSearch dispatches into SMW\MediaWiki\Specials\SpecialFacetedSearch::execute().
  2. Request decoding boundary:
    • SpecialFacetedSearch::execute() constructs UrlArgs from $request->getValues() and calls ParametersProcessor::checkRequest($request).
  3. Checksum gate:
    • ParametersProcessor::checkRequest() clears cstate only when filtered != 1 and getInt('csum', 0) != crc32(getVal('q', '')).
  4. Decoder -> HTML assembly:
    • HtmlBuilder::buildHTML() iterates foreach ( $urlArgs->getArray( 'cstate' ) as $key => $value ) and concatenates each into $hidden without escaping.
    • HtmlBuilder::buildHTML() passes $hidden into the template variable hidden.
  5. HTML injection sink:
    • templates/FacetedSearch/search.mustache renders {{{hidden}}} into the <form>, so the concatenated markup is inserted as raw HTML.

PoC

Reproduction steps (source-derived)

  1. Choose a q value.
  2. Compute csum as crc32(q).
  3. Send a request that includes:
    • q=<chosen>
    • csum=<crc32(q)>
    • at least one cstate[<key>]=<payload> entry

Example request shape:

/index.php/Special:FacetedSearch?q=Text&csum=<crc32(Text)>&cstate[0]=x%22%20autofocus%20onfocus%3Dalert(1)%20x%22

Impact

Attacker model

  • Any remote attacker who can send HTTP requests to Special:FacetedSearch (or the localized alias mapped to the same SpecialFacetedSearch class) can supply attacker-controlled query parameters.
  • Preconditions:
    • The attacker must make cstate survive ParametersProcessor::checkRequest(), either by setting csum to crc32(q) (when filtered != 1), or by setting filtered=1.
    • The attacker must supply cstate[<key>] values containing characters that break out of the HTML value="..." attribute context (for example an injected " to terminate the attribute value).

Severity and CVSS reasoning

Proposed severity: MEDIUM.

Proposed CVSS v3.1 vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N.

Rationale:

  • AV:N: delivered over the network via query parameters.
  • AC:L: requires only setting q, csum, and at least one cstate entry.
  • PR:N: no authentication required for the request path in this code.
  • UI:R: the victim must load the crafted URL.
  • S:C: reflected XSS executes in the wiki origin and can affect other users depending on deployment and browser behavior.

Why this is a residual of CVE-2025-10354

  • The CVE-2025-10354 hardening shipped by escaping the q parameter before emitting it into the value="{{q}}" attribute.
  • Commit 3d675ce updates only the q rendering to use htmlspecialchars( $urlArgs->get( 'q', '' ) ) and does not touch the adjacent cstate -> $hidden construction loop.
  • As a result, cstate remains an unescaped input source that flows into the same raw template injection point ({{{hidden}}}), creating a distinct reflected-XSS lane.

Output (from code inspection)

Given the payload idea where cstate[0] starts with x" ... x", HtmlBuilder.php constructs the hidden fragment by concatenation:

<input name="cstate[0]" type="hidden" value="x" autofocus onfocus=alert(1) x">

Because search.mustache injects the fragment via {{{hidden}}}, the attacker-controlled markup participates in normal HTML parsing in the response body.

Suggested fix

  • Escape both the cstate key and value when constructing $hidden.
  • Minimal code change in src/MediaWiki/Specials/FacetedSearch/HtmlBuilder.php:
foreach ( $urlArgs->getArray( 'cstate' ) as $key => $value ) {
	$safeKey = htmlspecialchars( (string)$key, ENT_QUOTES, 'UTF-8' );
	$safeValue = htmlspecialchars( (string)$value, ENT_QUOTES, 'UTF-8' );
	$hidden .= '<input name="cstate[' . $safeKey . ']" type="hidden" value="' . $safeValue . '">';
}

This keeps the raw {{{hidden}}} template insertion safe by ensuring the concatenated HTML fragment itself is attribute-escaped.

How I found it and a note on tooling

I anchored on the published CVE-2025-10354 patch by verifying in the checked-out repository that commit 3d675ce changes only the q rendering in HtmlBuilder.php to use htmlspecialchars.

Then I traced the reachable request path from SpecialFacetedSearch::execute() through ParametersProcessor::checkRequest() (checksum gate for whether cstate survives) into HtmlBuilder::buildHTML() where $hidden is constructed from cstate without escaping and injected into templates/FacetedSearch/search.mustache via {{{hidden}}}.

(End of file)

AI tooling

I used AI assistance for the code audit and for drafting this report. I manually verified the finding against the project's source at the location cited above before reporting it, and the severity and impact assessment are my own.

🎯 Affected products1

  • composer/mediawiki/semantic-media-wiki:>= 4.2.0, <= 7.2.0

🔗 References (3)