Merge pull request from GHSA-xv83-x443-7rmw

* Escape HTML for plaintext search results

* Add tests
pull/28217/head
Michael Telatynski 2023-04-25 09:30:32 +01:00 committed by GitHub
parent 619a9e8542
commit 961b843662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 9 deletions

View File

@ -28,6 +28,7 @@ import { decode } from "html-entities";
import { IContent } from "matrix-js-sdk/src/models/event"; import { IContent } from "matrix-js-sdk/src/models/event";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import _Linkify from "linkify-react"; import _Linkify from "linkify-react";
import escapeHtml from "escape-html";
import { import {
_linkifyElement, _linkifyElement,
@ -355,10 +356,10 @@ abstract class BaseHighlighter<T extends React.ReactNode> {
public constructor(public highlightClass: string, public highlightLink?: string) {} 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 * @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, * @param {string[]} safeHighlights A list of substrings to highlight,
* sorted by descending length. * sorted by descending length.
* *
@ -367,7 +368,7 @@ abstract class BaseHighlighter<T extends React.ReactNode> {
*/ */
public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] { public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] {
let lastOffset = 0; let lastOffset = 0;
let offset; let offset: number;
let nodes: T[] = []; let nodes: T[] = [];
const safeHighlight = safeHighlights[0]; const safeHighlight = safeHighlights[0];
@ -440,7 +441,7 @@ interface IOpts {
} }
export interface IOptsReturnNode extends IOpts { export interface IOptsReturnNode extends IOpts {
returnString: false | undefined; returnString?: false | undefined;
} }
export interface IOptsReturnString extends IOpts { export interface IOptsReturnString extends IOpts {
@ -574,7 +575,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
safeBody = formatEmojis(safeBody, true).join(""); safeBody = formatEmojis(safeBody, true).join("");
} }
} else if (highlighter) { } else if (highlighter) {
safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join(""); safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join("");
} }
} finally { } finally {
delete sanitizeParams.textFilter; delete sanitizeParams.textFilter;

View File

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

View File

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { render, screen } from "@testing-library/react"; 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"; import SettingsStore from "../src/settings/SettingsStore";
jest.mock("../src/settings/SettingsStore"); jest.mock("../src/settings/SettingsStore");
@ -29,7 +30,7 @@ const enableHtmlTopicFeature = () => {
}); });
}; };
describe("HtmlUtils", () => { describe("topicToHtml", () => {
function getContent() { function getContent() {
return screen.getByRole("contentinfo").children[0].innerHTML; 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>'); 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"`);
});
});