### 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:
```text
/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:
```html
<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`:
```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.