VYPR
Moderate severityNVD Advisory· Published Jul 18, 2023· Updated Oct 18, 2024

Cross site scripting in Export Chat feature

CVE-2023-37259

Description

matrix-react-sdk is a react-based SDK for inserting a Matrix chat/voip client into a web page. The Export Chat feature includes certain attacker-controlled elements in the generated document without sufficient escaping, leading to stored Cross site scripting (XSS). Since the Export Chat feature generates a separate document, an attacker can only inject code run from the null origin, restricting the impact. However, the attacker can still potentially use the XSS to leak message contents. A malicious homeserver is a potential attacker since the affected inputs are controllable server-side. This issue has been addressed in commit 22fcd34c60 which is included in release version 3.76.0. Users are advised to upgrade. The only known workaround for this issue is to disable or to not use the Export Chat feature.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2023-37259 is a stored cross-site scripting vulnerability in the Matrix React SDK's Export Chat feature, impacting users who export chat history by allowing attacker-controlled content to execute scripts from the null origin, potentially leaking message contents.

Vulnerability

Description

The matrix-react-sdk, a React-based SDK for integrating Matrix chat and VoIP clients into web pages, contains a stored cross-site scripting (XSS) vulnerability in its Export Chat feature [1]. The feature generates an HTML document from chat history but fails to properly escape attacker-controlled elements, allowing malicious content to be injected into the exported document without sufficient sanitization [1]. This issue arises because attacker-controlled inputs, such as room names, topics, and user display names, are included in the generated HTML without proper escaping, enabling script execution in the context of the exported document [3].

Exploitation and

Attack Vector

Exploitation requires the attacker to have control over server-side inputs, such as a malicious homeserver, which can manipulate room metadata or user display names [1]. The exported HTML document is generated as a separate file, which means injected scripts run from the null origin, limiting the attack surface to the document's own context [1]. Despite this restriction, the attacker can still execute arbitrary JavaScript within that scope, potentially exfiltrating sensitive information from the exported chat content [1]. The vulnerability is triggered when a user exports chat history using the affected versions of the SDK, as the malicious payload is embedded in the export output and executed when the exported document is opened [1].

Impact

An attacker exploiting this vulnerability can use the XSS to leak message contents from the exported chat, compromising the confidentiality of private conversations [1]. Since the attack operates from a null origin, it cannot interact with the main application or access cookies, but it can read and transmit the contents of the exported document, which may include sensitive messages, files, or user data [1]. The risk is particularly significant for users who rely on the Export Chat feature to archive or share conversations, as an attacker-controlled homeserver could poison the export output with malicious scripts [1].

Mitigation

The vulnerability has been addressed in commit 22fcd34c60 and is included in release version 3.76.0 of the matrix-react-sdk [1][3][4]. Users are strongly advised to upgrade to this version or later to apply the fix [1]. The patch introduces proper HTML escaping for all user-controlled strings that are used in constructing the exported document, including room names, topics, exporter identifiers, and display names [3]. For users unable to upgrade immediately, the only known workaround is to disable or avoid using the Export Chat feature entirely [1].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
matrix-react-sdknpm
>= 3.32.0, < 3.76.03.76.0

Affected products

2

Patches

1
22fcd34c606f

Sanitise strings going into the html export CVE-2023-37259

2 files changed · +71 16
  • src/utils/exportUtils/HtmlExport.tsx+25 16 modified
    @@ -21,6 +21,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
     import { renderToStaticMarkup } from "react-dom/server";
     import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
     import { logger } from "matrix-js-sdk/src/logger";
    +import escapeHtml from "escape-html";
     
     import Exporter from "./Exporter";
     import { mediaFromMxc } from "../../customisations/Media";
    @@ -97,28 +98,36 @@ export default class HTMLExporter extends Exporter {
             const exporter = this.room.client.getSafeUserId();
             const exporterName = this.room.getMember(exporter)?.rawDisplayName;
             const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
    -        const createdText = _t("%(creatorName)s created this room.", {
    -            creatorName,
    -        });
     
    -        const exportedText = renderToStaticMarkup(
    +        const safeCreatedText = escapeHtml(
    +            _t("%(creatorName)s created this room.", {
    +                creatorName,
    +            }),
    +        );
    +        const safeExporter = escapeHtml(exporter);
    +        const safeRoomName = escapeHtml(this.room.name);
    +        const safeTopic = escapeHtml(topic);
    +        const safeExportedText = renderToStaticMarkup(
                 <p>
                     {_t(
                         "This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.",
                         {
                             exportDate,
                         },
                         {
    -                        roomName: () => <b>{this.room.name}</b>,
    +                        roomName: () => <b>{safeRoomName}</b>,
                             exporterDetails: () => (
    -                            <a href={`https://matrix.to/#/${exporter}`} target="_blank" rel="noopener noreferrer">
    +                            <a
    +                                href={`https://matrix.to/#/${encodeURIComponent(exporter)}`}
    +                                target="_blank"
    +                                rel="noopener noreferrer"
    +                            >
                                     {exporterName ? (
                                         <>
    -                                        <b>{exporterName}</b>
    -                                        {" (" + exporter + ")"}
    +                                        <b>{escapeHtml(exporterName)}</b>I {" (" + safeExporter + ")"}
                                         </>
                                     ) : (
    -                                    <b>{exporter}</b>
    +                                    <b>{safeExporter}</b>
                                     )}
                                 </a>
                             ),
    @@ -127,7 +136,7 @@ export default class HTMLExporter extends Exporter {
                 </p>,
             );
     
    -        const topicText = topic ? _t("Topic: %(topic)s", { topic }) : "";
    +        const safeTopicText = topic ? _t("Topic: %(topic)s", { topic: safeTopic }) : "";
             const previousMessagesLink = renderToStaticMarkup(
                 currentPage !== 0 ? (
                     <div style={{ textAlign: "center" }}>
    @@ -183,12 +192,12 @@ export default class HTMLExporter extends Exporter {
                                 <div
                                     dir="auto"
                                     class="mx_RoomHeader_nametext"
    -                                title="${this.room.name}"
    +                                title="${safeRoomName}"
                                 >
    -                                ${this.room.name}
    +                                ${safeRoomName}
                                 </div>
                                 </div>
    -                            <div class="mx_RoomHeader_topic" dir="auto"> ${topic} </div>
    +                            <div class="mx_RoomHeader_topic" dir="auto"> ${safeTopic} </div>
                             </div>
                             </div>
                             ${previousMessagesLink}
    @@ -214,10 +223,10 @@ export default class HTMLExporter extends Exporter {
                                         currentPage == 0
                                             ? `<div class="mx_NewRoomIntro">
                                             ${roomAvatar}
    -                                        <h2> ${this.room.name} </h2>
    -                                        <p> ${createdText} <br/><br/> ${exportedText} </p>
    +                                        <h2> ${safeRoomName} </h2>
    +                                        <p> ${safeCreatedText} <br/><br/> ${safeExportedText} </p>
                                             <br/>
    -                                        <p> ${topicText} </p>
    +                                        <p> ${safeTopicText} </p>
                                         </div>`
                                             : ""
                                     }
    
  • test/utils/exportUtils/HTMLExport-test.ts+46 0 modified
    @@ -25,6 +25,7 @@ import {
         RoomState,
     } from "matrix-js-sdk/src/matrix";
     import fetchMock from "fetch-mock-jest";
    +import escapeHtml from "escape-html";
     
     import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
     import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
    @@ -505,4 +506,49 @@ describe("HTMLExport", () => {
             );
             expect(result).not.toContain("Next group of messages");
         });
    +
    +    it("should not leak javascript from room names or topics", async () => {
    +        const name = "<svg onload=alert(3)>";
    +        const topic = "<svg onload=alert(5)>";
    +        mockMessages(EVENT_MESSAGE);
    +        room.currentState.setStateEvents([
    +            new MatrixEvent({
    +                type: EventType.RoomName,
    +                event_id: "$00001",
    +                room_id: room.roomId,
    +                sender: "@alice:example.com",
    +                origin_server_ts: 0,
    +                content: { name },
    +                state_key: "",
    +            }),
    +            new MatrixEvent({
    +                type: EventType.RoomTopic,
    +                event_id: "$00002",
    +                room_id: room.roomId,
    +                sender: "@alice:example.com",
    +                origin_server_ts: 1,
    +                content: { topic },
    +                state_key: "",
    +            }),
    +        ]);
    +        room.recalculate();
    +
    +        const exporter = new HTMLExporter(
    +            room,
    +            ExportType.Timeline,
    +            {
    +                attachmentsIncluded: false,
    +                maxSize: 1_024 * 1_024,
    +            },
    +            () => {},
    +        );
    +
    +        await exporter.export();
    +        const html = await getMessageFile(exporter).text();
    +
    +        expect(html).not.toContain(`${name}`);
    +        expect(html).toContain(`${escapeHtml(name)}`);
    +        expect(html).not.toContain(`${topic}`);
    +        expect(html).toContain(`Topic: ${escapeHtml(topic)}`);
    +    });
     });
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.