Element Web
Unsanitized HTML Injection via the Homeserver-Controlled Home Page URL
The EmbeddedPage component fetches the home page from a configurable URL and renders the response through dangerouslySetInnerHTML without sanitizing it. When a deployment does not pin embedded_pages.home_url in its own config, that URL is read from the homeserver's .well-known/matrix/client document, so a hostile homeserver operator can place arbitrary markup in the client of every user who logs in against that server. The Content Security Policy blocks script execution, but frame-src * leaves credential phishing through an injected iframe.
Description
EmbeddedPage fetches the configured home page over HTTP and stores the response body in component state. The sanitize-html library is imported in the same file, but only for the translation-string substitution on the way in. The body itself never passes through it.
EmbeddedPage.tsx:76-88 (v1.12.21)
// Replace '," and HTML encoded variantslet body = (await res.text()).replace( /_t\((?:['"]|(?:&#(?:34|27);))([\s\S]*?)(?:['"]|(?:&#(?:34|27);))\)/gm, (match, g1) => this.translate(g1),);
if (this.props.replaceMap) { Object.keys(this.props.replaceMap).forEach((key) => { body = body.split(key).join(this.props.replaceMap![key]); });}
this.setState({ page: body });The stored body is then rendered straight into the component tree:
EmbeddedPage.tsx:128 (v1.12.21)
const content = <div className={`${className}_body`} dangerouslySetInnerHTML={{ __html: this.state.page }} />;The URL comes from getHomePageUrl(), which prefers the deployment's own config.json and falls back to the homeserver when that key is absent:
pages.ts:15-26 (v1.12.21)
export function getHomePageUrl(appConfig: IConfigOptions, matrixClient: MatrixClient): string | undefined { const config = new SnakedObject(appConfig);
const pagesConfig = config.get("embedded_pages"); let pageUrl = pagesConfig ? new SnakedObject(pagesConfig).get("home_url") : null;
if (!pageUrl) { pageUrl = getEmbeddedPagesWellKnown(matrixClient)?.home_url; }
return pageUrl;}WellKnownUtils.ts:16, 68-74 (v1.12.21)
const EMBEDDED_PAGES_WK_PROPERTY = "io.element.embedded_pages";
export function getEmbeddedPagesWellKnown(matrixClient: MatrixClient | undefined): IEmbeddedPagesWellKnown | undefined { return embeddedPagesFromWellKnown(matrixClient?.getClientWellKnown());}
export function embeddedPagesFromWellKnown(clientWellKnown?: IClientWellKnown): IEmbeddedPagesWellKnown { return clientWellKnown?.[EMBEDDED_PAGES_WK_PROPERTY];}The read is a bare property lookup: no validation, no origin pinning beyond the client's own homeserver. getEmbeddedPagesWellKnown() returns io.element.embedded_pages.home_url from the .well-known/matrix/client document served by that homeserver. That fallback was added to matrix-react-sdk in pull request #7790; the unsanitized render predates it. A deployment that sets embedded_pages.home_url in its config.json never reaches the fallback, so exposure depends on the deployment's configuration as much as on the client version.
The injection does not become script execution. The shipped Content Security Policy is the reason, and it also decides what is left:
| Directive | Value | Consequence |
|---|---|---|
script-src | 'self' 'wasm-unsafe-eval' (plus reCAPTCHA) | Inline scripts blocked. Not XSS. |
frame-src | * blob: data: | An attacker iframe inside the trusted interface. This is the phishing primitive. |
form-action | 'self' | Blocks a direct POST from the injected markup, but not one from inside the iframe. |
style-src | 'self' 'unsafe-inline' | Injected style blocks restyle the surrounding interface. |
img-src | * blob: data: | Tracking pixels and CSS-based beacons. |
connect-src | * blob: | The home page body may be fetched from any origin. |
base-uri | not set | No restriction on an injected base tag. Not exploitable here; a defense-in-depth gap. |
Script execution was ruled out, not assumed. Three separate passes looked for an escalation to JavaScript and found none. DOM clobbering fails because every candidate element ID (mx_Dialog_Container, mx_ContextualMenu_Container, and the rest) already exists in index.html ahead of the injection point, so document.getElementById returns the pre-existing node, and the window.mx* singletons are initialized at module load, before EmbeddedPage renders. No script gadget is reachable: React 19 mounts with createRoot rather than hydration, wasm-unsafe-eval needs JavaScript to already be running, the origin serves no JSONP or reflecting endpoint, and the bundled webpack (5.107.1 in the audited release) is patched against base-tag clobbering (CVE-2024-43788, fixed upstream in 5.94.0). A <meta http-equiv> element has no effect when inserted through innerHTML. The postMessage handlers validate their origin (ScalarMessaging) or their event.source (SSO and fallback auth), and no ClientWidgetApi instance is live on the home page.
Scope. The well-known document is read from the user's own homeserver only, through matrixClient.getClientWellKnown(). Well-known data does not propagate over federation, so a hostile server on the far side of a federated room cannot reach clients that authenticate elsewhere. The attacker is the operator of the server the user has already chosen, or whoever has taken that server over.
Reported to Element on 2026-04-01. The fix landed on 2026-06-15 in pull request #33842 and shipped in 1.12.22: the embedded page now renders through the shared sanitizedHtmlNode helper instead of dangerouslySetInnerHTML.
Impact
- A hostile homeserver operator, or anyone who has taken one over, can put attacker-controlled markup on the home page of every Element Web client that authenticates against that server. The home page is the default view after login and after registration and stays reachable from the Home button, so the injected content is persistent rather than a single shot.
- Script execution is blocked, so messages, access tokens, and key material stay out of reach. What remains is credential phishing.
frame-src *admits a full-viewport iframe from any origin, and a form inside that iframe has its own browsing context, so the parent'sform-action 'self'does not constrain where it submits. A session-expired dialog styled like the surrounding client collects the user's password and sends it to the attacker, inside an application the user has already decided to trust. We confirmed this end to end against Element Web v1.11.96. img-src *additionally allows tracking pixels that report when and how often a given user opens the client, andstyle-src 'unsafe-inline'lets the injected markup restyle the interface around it.
Mitigation
Update Element Web or Element Desktop to 1.12.22 or later. The fix renders the embedded page through the shared sanitizedHtmlNode helper, which strips the tags the injection depends on. Deployments that cannot update immediately should set embedded_pages.home_url explicitly in config.json, which stops the resolver from consulting the homeserver-controlled well-known key at all. Independently of the version, narrowing frame-src to 'self' plus the widget and call origins actually in use removes the phishing primitive, and adding base-uri 'none' closes a defense-in-depth gap, since base-uri does not fall back to default-src. For people using a client they do not administer: the home page has no legitimate reason to ask for a password, so treat any login prompt that appears there as suspicious until the client is updated.
Defender's Checklist
Update to Element Web or Element Desktop 1.12.22 or later.
Every earlier release renders the embedded home page unsanitized. The fix is pull request #33842.
Pin embedded_pages.home_url in your own config.
getHomePageUrl()only consults the homeserver's well-known when the deployment config leaves the key unset. Setting it inconfig.jsontakes the homeserver out of the trust path for the home page, in any version. Check theconfig.jsonyou actually deploy, not the sample.Tighten the deployment CSP.
frame-src *is what turns markup injection into a phishing surface, because a form inside a third-party iframe is not bound by the parent'sform-action 'self'. Narrow it to'self'plus the widget and call origins you actually use, and addbase-uri 'none', which does not fall back todefault-src.Check forks and custom home pages.
Products built on the Element Web codebase inherit both the unsanitized render and the well-known fallback. If you maintain a fork, or ship your own home page, confirm the fix is merged and that your deployment pins
home_url.
Severity Reasoning
.well-known/matrix/client response. No timing, no race, no per-target preparation.PR:HRequires administrative control of the homeserver the user authenticates against, as its operator or after compromising it.UI:NThe client fetches and renders the home page on its own after login.S:CThe vulnerable component is the web client. The injected iframe runs in a browsing context the client's own form-action no longer constrains, and the credential it collects authenticates beyond the rendering component.C:LLoaded resources reveal that a targeted user opened the client and when. Message content and key material stay behind the CSP.I:HThe attacker fully controls the markup rendered in a trusted surface of the application, up to a full-viewport overlay.A:NNo availability impact.Element rated the advisory Moderate. The disagreement is the scope metric: the same vector with S:U scores 5.5. We score S:C because the credential the phishing dialog collects is not confined to the component that renders the dialog. Both precedents score S:C on the same client trust boundary: CVE-2024-42347 (a homeserver enabling URL previews in encrypted rooms, 7.7) and CVE-2023-30609 (HTML injection in matrix-react-sdk search results, 5.4).
References
- GHSA-wrcp-5v3v-3j6v (Element)
- CVE-2026-55850 (NVD)
- Element Web v1.12.22 Release
- Element Web pull request #33842 (fix)
- matrix-react-sdk pull request #7790 (origin of the well-known fallback)
- Element Web Repository
- CVE-2024-42347 (NVD, S:C scoring precedent)
- CVE-2023-30609 (NVD, S:C scoring precedent)
- Related: Element X Android crash via a malformed OIDC redirect intent
How We Can Help
Who We Are
The security researchers behind this advisory.

Dr. rer. nat. Simon Weber
Senior Pentester & MedSec Researcher
I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.
- PhD on Hospital Cybersecurity
- Critical vulnerabilities found in hospital systems
- Alumni of THB MedSec Research Group
- gematik Security Hero

Dipl.-Inf. Volker Schönefeld
Senior Application Security Expert
As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.
- 20+ years as CTO, 50M+ app downloads
- Architected and secured large-scale IoT fleets
- Certified Web Exploitation Specialist
- gematik Security Hero
Looking for a Penetration Test?
Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.
