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/SemanticMediaWiki@7.2.0.
- 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)
- HTTP entrypoint:
GET to Special:FacetedSearch dispatches into SMW\MediaWiki\Specials\SpecialFacetedSearch::execute().
- Request decoding boundary:
SpecialFacetedSearch::execute() constructs UrlArgs from $request->getValues() and calls ParametersProcessor::checkRequest($request).
- Checksum gate:
ParametersProcessor::checkRequest() clears cstate only when filtered != 1 and getInt('csum', 0) != crc32(getVal('q', '')).
- 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.
- 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)
- Choose a
q value.
- Compute
csum as crc32(q).
- 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.
- 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.
References
Summary
Special:FacetedSearch
cstatehidden inputs enable reflected XSS (residual of CVE-2025-10354)Details
Affected versions and vulnerable location
SemanticMediaWiki/SemanticMediaWiki@7.2.0.masterat HEAD18f418b4cdf2875e67a741349179a22c1573f61c.Vulnerable sink (default-branch representation):
src/MediaWiki/Specials/FacetedSearch/HtmlBuilder.php:131-133$hiddenby concatenating unescaped request-controlledcstate[$key]values into an HTML attribute context (value="...").templates/FacetedSearch/search.mustache:25{{{hidden}}}(no HTML escaping at this boundary).Reachability trace (verified from source)
GETtoSpecial:FacetedSearchdispatches intoSMW\MediaWiki\Specials\SpecialFacetedSearch::execute().SpecialFacetedSearch::execute()constructsUrlArgsfrom$request->getValues()and callsParametersProcessor::checkRequest($request).ParametersProcessor::checkRequest()clearscstateonly whenfiltered != 1andgetInt('csum', 0) != crc32(getVal('q', '')).HtmlBuilder::buildHTML()iteratesforeach ( $urlArgs->getArray( 'cstate' ) as $key => $value )and concatenates each into$hiddenwithout escaping.HtmlBuilder::buildHTML()passes$hiddeninto the template variablehidden.templates/FacetedSearch/search.mustacherenders{{{hidden}}}into the<form>, so the concatenated markup is inserted as raw HTML.PoC
Reproduction steps (source-derived)
qvalue.csumascrc32(q).q=<chosen>csum=<crc32(q)>cstate[<key>]=<payload>entryExample request shape:
Impact
Attacker model
Special:FacetedSearch(or the localized alias mapped to the sameSpecialFacetedSearchclass) can supply attacker-controlled query parameters.cstatesurviveParametersProcessor::checkRequest(), either by settingcsumtocrc32(q)(whenfiltered != 1), or by settingfiltered=1.cstate[<key>]values containing characters that break out of the HTMLvalue="..."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:
q,csum, and at least onecstateentry.Why this is a residual of CVE-2025-10354
qparameter before emitting it into thevalue="{{q}}"attribute.3d675ceupdates only theqrendering to usehtmlspecialchars( $urlArgs->get( 'q', '' ) )and does not touch the adjacentcstate->$hiddenconstruction loop.cstateremains 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 withx" ... x",HtmlBuilder.phpconstructs the hidden fragment by concatenation:Because
search.mustacheinjects the fragment via{{{hidden}}}, the attacker-controlled markup participates in normal HTML parsing in the response body.Suggested fix
cstatekey and value when constructing$hidden.src/MediaWiki/Specials/FacetedSearch/HtmlBuilder.php: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
3d675cechanges only theqrendering inHtmlBuilder.phpto usehtmlspecialchars.Then I traced the reachable request path from
SpecialFacetedSearch::execute()throughParametersProcessor::checkRequest()(checksum gate for whethercstatesurvives) intoHtmlBuilder::buildHTML()where$hiddenis constructed fromcstatewithout escaping and injected intotemplates/FacetedSearch/search.mustachevia{{{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.
References