CVE-2023-30609 (published 2023-04-25T21:15Z)

matrix-react-sdk is a react-based SDK for inserting a Matrix chat/VoIP client into a web page. Prior to version 3.71.0, plain text messages containing HTML tags are rendered as HTML in the search results. To exploit this, an attacker needs to trick a user into searching for a specific message containing an HTML injection payload. No cross-site scripting attack is possible due to the hardcoded content security policy. Version 3.71.0 of the SDK patches over the issue. As a workaround, restarting the client will clear the HTML injection.

CVSS v3 base score: 4.7, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/matrix-org/matrix-react-sdk
commit_details: repo_url: https://github.com/matrix-org/matrix-react-sdk
author: Michael Telatynski
author_date: 2023-04-25 09:30:32+01:00
msg: Merge pull request from GHSA-xv83-x443-7rmw

* Escape HTML for plaintext search results

* Add tests
repository: repo_url: https://github.com/matrix-org/matrix-react-sdk
repo_name: matrix-org/matrix-react-sdk
description: Matrix SDK for React Javascript
repo_language: TypeScript
owner: matrix-org
file_changes: filename: HtmlUtils.tsx
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ import { decode } from "html-entities";
 import { IContent } from "matrix-js-sdk/src/models/event";
 import { Optional } from "matrix-events-sdk";
 import _Linkify from "linkify-react";
+import escapeHtml from "escape-html";
 
 import {
     _linkifyElement,
@@ -355,10 +356,10 @@ abstract class BaseHighlighter<T extends React.ReactNode> {
     public constructor(public highlightClass: string, public highlightLink?: string) {}
 
     /**
-     * apply the highlights to a section of text
+     * Apply the highlights to a section of text
      *
      * @param {string} safeSnippet The snippet of text to apply the highlights
-     *     to.
+     *     to. This input must be sanitised as it will be treated as HTML.
      * @param {string[]} safeHighlights A list of substrings to highlight,
      *     sorted by descending length.
      *
@@ -367,7 +368,7 @@ abstract class BaseHighlighter<T extends React.ReactNode> {
      */
     public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] {
         let lastOffset = 0;
-        let offset;
+        let offset: number;
         let nodes: T[] = [];
 
         const safeHighlight = safeHighlights[0];
@@ -440,7 +441,7 @@ interface IOpts {
 }
 
 export interface IOptsReturnNode extends IOpts {
-    returnString: false | undefined;
+    returnString?: false | undefined;
 }
 
 export interface IOptsReturnString extends IOpts {
@@ -574,7 +575,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
                 safeBody = formatEmojis(safeBody, true).join("");
             }
         } else if (highlighter) {
-            safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join("");
+            safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join("");
         }
     } finally {
         delete sanitizeParams.textFilter;

programming_language: TypeScript
filename: SearchResultTile.tsx
change_type: ModificationType.MODIFY
diff: @@ -71,7 +71,7 @@ export default class SearchResultTile extends React.Component<IProps> {
 
         for (let j = 0; j < timeline.length; j++) {
             const mxEv = timeline[j];
-            let highlights;
+            let highlights: string[] | undefined;
             const contextual = !this.props.ourEventsIndexes.includes(j);
             if (!contextual) {
                 highlights = this.props.searchHighlights;

programming_language: TypeScript
filename: HtmlUtils-test.tsx
change_type: ModificationType.MODIFY
diff: @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
+import React, { ReactElement } from "react";
 import { mocked } from "jest-mock";
 import { render, screen } from "@testing-library/react";
+import { IContent } from "matrix-js-sdk/src/models/event";
 
-import { topicToHtml } from "../src/HtmlUtils";
+import { bodyToHtml, topicToHtml } from "../src/HtmlUtils";
 import SettingsStore from "../src/settings/SettingsStore";
 
 jest.mock("../src/settings/SettingsStore");
@@ -29,7 +30,7 @@ const enableHtmlTopicFeature = () => {
     });
 };
 
-describe("HtmlUtils", () => {
+describe("topicToHtml", () => {
     function getContent() {
         return screen.getByRole("contentinfo").children[0].innerHTML;
     }
@@ -62,3 +63,47 @@ describe("HtmlUtils", () => {
         expect(getContent()).toEqual('<b>pizza</b> <span class="mx_Emoji" title=":pizza:">🍕</span>');
     });
 });
+
+describe("bodyToHtml", () => {
+    function getHtml(content: IContent, highlights?: string[]): string {
+        return (bodyToHtml(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html;
+    }
+
+    it("should apply highlights to HTML messages", () => {
+        const html = getHtml(
+            {
+                body: "test **foo** bar",
+                msgtype: "m.text",
+                formatted_body: "test <b>foo</b> bar",
+                format: "org.matrix.custom.html",
+            },
+            ["test"],
+        );
+
+        expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> <b>foo</b> bar"`);
+    });
+
+    it("should apply highlights to plaintext messages", () => {
+        const html = getHtml(
+            {
+                body: "test foo bar",
+                msgtype: "m.text",
+            },
+            ["test"],
+        );
+
+        expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> foo bar"`);
+    });
+
+    it("should not respect HTML tags in plaintext message highlighting", () => {
+        const html = getHtml(
+            {
+                body: "test foo <b>bar",
+                msgtype: "m.text",
+            },
+            ["test"],
+        );
+
+        expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> foo &lt;b&gt;bar"`);
+    });
+});

programming_language: TypeScript