From 7949980a7e3c7e397d7afe899ef1b0563c417b0e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 15 Jun 2026 13:58:56 +0100
Subject: [PATCH] Apply html utils sanitiser to embedded page (#33842)
* Apply html utils sanitiser to embedded page
* Write tests
---
apps/web/src/HtmlUtils.tsx | 13 ++++++++---
.../components/structures/EmbeddedPage.tsx | 13 ++++++++++-
apps/web/test/unit-tests/HtmlUtils-test.tsx | 13 +++++++++--
.../__snapshots__/HtmlUtils-test.tsx.snap | 17 ++++++++++++++
.../views/context_menus/EmbeddedPage-test.tsx | 9 ++++++++
.../__snapshots__/EmbeddedPage-test.tsx.snap | 23 +++++++++++++++++++
6 files changed, 82 insertions(+), 6 deletions(-)
diff --git a/apps/web/src/HtmlUtils.tsx b/apps/web/src/HtmlUtils.tsx
index 3fefbba418..abf092cbb8 100644
--- a/apps/web/src/HtmlUtils.tsx
+++ b/apps/web/src/HtmlUtils.tsx
@@ -89,11 +89,18 @@ export function unicodeToShortcode(char: string): string {
/*
* Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML.
+ * @param insaneHtml - the input to sanitize
+ * @param className - an optional class name to apply to the element
+ * @param sanitizeParams - the params to use for sanitization
*/
-export function sanitizedHtmlNode(insaneHtml: string): ReactNode {
- const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
+export function sanitizedHtmlNode(
+ insaneHtml: string,
+ className?: string,
+ sanitizeParams = sanitizeHtmlParams,
+): ReactNode {
+ const saneHtml = sanitizeHtml(insaneHtml, sanitizeParams);
- return
;
+ return ;
}
export function getHtmlText(insaneHtml: string): string {
diff --git a/apps/web/src/components/structures/EmbeddedPage.tsx b/apps/web/src/components/structures/EmbeddedPage.tsx
index a363dc7b5d..4f2bd2f6a6 100644
--- a/apps/web/src/components/structures/EmbeddedPage.tsx
+++ b/apps/web/src/components/structures/EmbeddedPage.tsx
@@ -19,6 +19,9 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { type ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions.ts";
+import { sanitizedHtmlNode } from "../../HtmlUtils.tsx";
+import { sanitizeHtmlParams, transformTags } from "../../Linkify.ts";
+import { objectExcluding } from "../../utils/objects.ts";
interface IProps {
// URL to request embedded page content from
@@ -126,7 +129,15 @@ export default class EmbeddedPage extends React.PureComponent {
[`${className}_loggedIn`]: !!client,
});
- const content = ;
+ const content = sanitizedHtmlNode(this.state.page, `${className}_body`, {
+ ...sanitizeHtmlParams,
+ transformTags: objectExcluding(transformTags, [
+ // Disable the transformer for `img` as it only allows mxc resources
+ "img",
+ // Disable the default transformer as it forbids inline styles
+ "*",
+ ]),
+ });
if (this.props.scrollbar) {
return {content};
diff --git a/apps/web/test/unit-tests/HtmlUtils-test.tsx b/apps/web/test/unit-tests/HtmlUtils-test.tsx
index e56d7203cc..3c71bdfa70 100644
--- a/apps/web/test/unit-tests/HtmlUtils-test.tsx
+++ b/apps/web/test/unit-tests/HtmlUtils-test.tsx
@@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
-import React from "react";
+import React, { type ReactElement } from "react";
import { render, screen } from "jest-matrix-react";
import parse from "html-react-parser";
-import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
+import { bodyToHtml, bodyToNode, formatEmojis, sanitizedHtmlNode, topicToHtml } from "../../src/HtmlUtils";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils";
import { SettingLevel } from "../../src/settings/SettingLevel";
@@ -319,3 +319,12 @@ describe("bodyToNode", () => {
jest.resetAllMocks();
});
});
+
+describe("sanitizedHtmlNode", () => {
+ it("should respect className", () => {
+ const sanitized = sanitizedHtmlNode('Link', "testClass");
+ const { asFragment, getByText } = render(sanitized as ReactElement);
+ expect(getByText("Link").parentNode).toHaveClass("testClass");
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/apps/web/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap b/apps/web/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap
index cca4c999cc..d4bdea290b 100644
--- a/apps/web/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap
+++ b/apps/web/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap
@@ -91,3 +91,20 @@ exports[`bodyToNode should handle inline media when mediaIsVisible is true 1`] =
`;
+
+exports[`sanitizedHtmlNode should respect className 1`] = `
+
+
+
+`;
diff --git a/apps/web/test/unit-tests/components/views/context_menus/EmbeddedPage-test.tsx b/apps/web/test/unit-tests/components/views/context_menus/EmbeddedPage-test.tsx
index 925928cf21..7f92ffa6a8 100644
--- a/apps/web/test/unit-tests/components/views/context_menus/EmbeddedPage-test.tsx
+++ b/apps/web/test/unit-tests/components/views/context_menus/EmbeddedPage-test.tsx
@@ -47,4 +47,13 @@ describe("", () => {
const { asFragment } = render();
expect(asFragment()).toMatchSnapshot();
});
+
+ it("should sanitise input", async () => {
+ fetchMock.get("https://other.page", `Foo
`);
+
+ const { asFragment } = render();
+ await expect(screen.findByText("Foo")).resolves.toBeVisible();
+ expect(screen.queryByRole("iframe")).not.toBeInTheDocument();
+ expect(asFragment()).toMatchSnapshot();
+ });
});
diff --git a/apps/web/test/unit-tests/components/views/context_menus/__snapshots__/EmbeddedPage-test.tsx.snap b/apps/web/test/unit-tests/components/views/context_menus/__snapshots__/EmbeddedPage-test.tsx.snap
index 8d7b6add45..f069425bbc 100644
--- a/apps/web/test/unit-tests/components/views/context_menus/__snapshots__/EmbeddedPage-test.tsx.snap
+++ b/apps/web/test/unit-tests/components/views/context_menus/__snapshots__/EmbeddedPage-test.tsx.snap
@@ -7,11 +7,29 @@ exports[` should render nothing if no url given 1`] = `
>
`;
+exports[` should sanitise input 1`] = `
+
+
+
+`;
+
exports[` should show error if unable to load 1`] = `
should show error if unable to load 1`] = `
>
Couldn't load page
@@ -33,6 +52,7 @@ exports[` should translate _t strings ["] 1`] = `
>
Przeglądaj pokoje
@@ -49,6 +69,7 @@ exports[` should translate _t strings [] 1`] = `
>
Przeglądaj pokoje
@@ -65,6 +86,7 @@ exports[` should translate _t strings ["] 1`] = `
>
Przeglądaj pokoje
@@ -81,6 +103,7 @@ exports[` should translate _t strings ['] 1`] = `
>
Przeglądaj pokoje