Element Web: Stored XSS in the HTML Chat Export

Any room member plants script code that runs as soon as another user opens the room's HTML export.

Advisory ID: TP-2026-108
Product: Element Web / Element Desktop (Matrix client for browser and desktop)
Vulnerability type: Stored cross-site scripting in the HTML export (CWE-79, CWE-116)
CVE: pending
CVSS 3.1: 6.3 (Medium), our rating; vendor severity: Moderate
Affected versions: <= 1.12.27
Vendor advisory: GHSA-wqmv-r2qj-2j9p
Fixed in: 1.12.28
Reported: 2 June 2026

Summary

Element is the most widely used client for the Matrix protocol and can export whole rooms as an HTML archive. The HTML export copied rendered message markup and attachment filenames into messages.html without sanitizing them again, and that file sets no Content Security Policy. turingpoint reported two ways for any room member to plant an executable <script> in another user's export: a message wrapped in <xmp> that passes the bundled sanitizer sanitize-html 2.17.3, and an attachment whose filename breaks out of the href attribute. The vendor advisory names unsanitised attachment filenames and Matrix IDs as the cause; the <xmp> path was already closed by the sanitize-html 2.17.4 update in version 1.12.21. When the victim opens the export, the attacker's script runs in the exported document and reads the entire exported chat history.

Root cause

The HTML export renders text messages through the regular EventTile component into a temporary element and copies its innerHTML verbatim into the archive (apps/web/src/utils/exportUtils/HtmlExport.tsx:337-341), while the generated <head> contains no Content Security Policy (HtmlExport.tsx:159). Message HTML is cleaned with sanitize-html (apps/web/src/HtmlUtils.tsx:136-155), and version 2.17.3 emits the content of a discarded <xmp> element unescaped (CVE-2026-44990), so a <script> wrapped in it lands in the export as a real element. With "Include attachments" enabled, getFilePath builds the file path from the attachment's free-form body via splitFileName without escaping it (apps/web/src/utils/exportUtils/Exporter.ts:283). HtmlExport.tsx:349 then swaps the mxc URL in the finished markup with split(mxc).join(filePath) as a plain string, so a " in the filename closes the href attribute and arbitrary HTML follows. Version 1.12.28 names exported files by event ID and MIME type and escapes the avatar path derived from the Matrix ID (fix commit).

Proof of Concept

PUT /_matrix/client/v3/rooms/<room>/send/m.room.message/<txn1>
{"msgtype": "m.text", "body": "x", "format": "org.matrix.custom.html",
 "formatted_body": "<xmp><script>document.title='XSS'</script></xmp>"}

PUT /_matrix/client/v3/rooms/<room>/send/m.room.message/<txn2>
{"msgtype": "m.file", "url": "mxc://<server>/<media-id>",
 "body": "x\"><script>document.title='XSS2'</script>y.png"}

Victim: Room info -> Export chat -> format HTML
        (for message 2 also tick "Include attachments")
        -> open messages.html -> document.title changes to XSS / XSS2

Message 1 passes the sanitizer through the <xmp> passthrough, message 2 breaks out of the link attribute when the file path is substituted. Because messages.html ships no CSP, the browser runs both scripts; the payload has to be free of URLs because the export linkifies them afterwards.

Impact

  • Execution of attacker JavaScript in the exported document as soon as the victim opens the export.
  • Reading and exfiltrating the entire exported chat history, for encrypted rooms in already decrypted plaintext.
  • Forging content in an export that is often kept as evidence or archive.
  • No access to the live Element session: the document runs in its own origin without a session token.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.