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

pull/28788/head^2
RiotRobot 2023-07-18 13:23:27 +01:00
parent d8dcfc96cc
commit 22fcd34c60
2 changed files with 71 additions and 16 deletions

View File

@ -21,6 +21,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import escapeHtml from "escape-html";
import Exporter from "./Exporter"; import Exporter from "./Exporter";
import { mediaFromMxc } from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";
@ -97,11 +98,16 @@ export default class HTMLExporter extends Exporter {
const exporter = this.room.client.getSafeUserId(); const exporter = this.room.client.getSafeUserId();
const exporterName = this.room.getMember(exporter)?.rawDisplayName; const exporterName = this.room.getMember(exporter)?.rawDisplayName;
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || ""; 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> <p>
{_t( {_t(
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.", "This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.",
@ -109,16 +115,19 @@ export default class HTMLExporter extends Exporter {
exportDate, exportDate,
}, },
{ {
roomName: () => <b>{this.room.name}</b>, roomName: () => <b>{safeRoomName}</b>,
exporterDetails: () => ( 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 ? ( {exporterName ? (
<> <>
<b>{exporterName}</b> <b>{escapeHtml(exporterName)}</b>I {" (" + safeExporter + ")"}
{" (" + exporter + ")"}
</> </>
) : ( ) : (
<b>{exporter}</b> <b>{safeExporter}</b>
)} )}
</a> </a>
), ),
@ -127,7 +136,7 @@ export default class HTMLExporter extends Exporter {
</p>, </p>,
); );
const topicText = topic ? _t("Topic: %(topic)s", { topic }) : ""; const safeTopicText = topic ? _t("Topic: %(topic)s", { topic: safeTopic }) : "";
const previousMessagesLink = renderToStaticMarkup( const previousMessagesLink = renderToStaticMarkup(
currentPage !== 0 ? ( currentPage !== 0 ? (
<div style={{ textAlign: "center" }}> <div style={{ textAlign: "center" }}>
@ -183,12 +192,12 @@ export default class HTMLExporter extends Exporter {
<div <div
dir="auto" dir="auto"
class="mx_RoomHeader_nametext" class="mx_RoomHeader_nametext"
title="${this.room.name}" title="${safeRoomName}"
> >
${this.room.name} ${safeRoomName}
</div> </div>
</div> </div>
<div class="mx_RoomHeader_topic" dir="auto"> ${topic} </div> <div class="mx_RoomHeader_topic" dir="auto"> ${safeTopic} </div>
</div> </div>
</div> </div>
${previousMessagesLink} ${previousMessagesLink}
@ -214,10 +223,10 @@ export default class HTMLExporter extends Exporter {
currentPage == 0 currentPage == 0
? `<div class="mx_NewRoomIntro"> ? `<div class="mx_NewRoomIntro">
${roomAvatar} ${roomAvatar}
<h2> ${this.room.name} </h2> <h2> ${safeRoomName} </h2>
<p> ${createdText} <br/><br/> ${exportedText} </p> <p> ${safeCreatedText} <br/><br/> ${safeExportedText} </p>
<br/> <br/>
<p> ${topicText} </p> <p> ${safeTopicText} </p>
</div>` </div>`
: "" : ""
} }

View File

@ -25,6 +25,7 @@ import {
RoomState, RoomState,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import escapeHtml from "escape-html";
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils"; import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils"; import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
@ -505,4 +506,49 @@ describe("HTMLExport", () => {
); );
expect(result).not.toContain("Next group of messages"); 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)}`);
});
}); });