Apply html utils sanitiser to embedded page (#33842)
* Apply html utils sanitiser to embedded page * Write tests
This commit is contained in:
@@ -89,11 +89,18 @@ export function unicodeToShortcode(char: string): string {
|
|||||||
/*
|
/*
|
||||||
* Given an untrusted HTML string, return a React node with an sanitized version
|
* Given an untrusted HTML string, return a React node with an sanitized version
|
||||||
* of that HTML.
|
* 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 {
|
export function sanitizedHtmlNode(
|
||||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
insaneHtml: string,
|
||||||
|
className?: string,
|
||||||
|
sanitizeParams = sanitizeHtmlParams,
|
||||||
|
): ReactNode {
|
||||||
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeParams);
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHtmlText(insaneHtml: string): string {
|
export function getHtmlText(insaneHtml: string): string {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { type ActionPayload } from "../../dispatcher/payloads";
|
import { type ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { Action } from "../../dispatcher/actions.ts";
|
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 {
|
interface IProps {
|
||||||
// URL to request embedded page content from
|
// URL to request embedded page content from
|
||||||
@@ -126,7 +129,15 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
|||||||
[`${className}_loggedIn`]: !!client,
|
[`${className}_loggedIn`]: !!client,
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = <div className={`${className}_body`} dangerouslySetInnerHTML={{ __html: this.state.page }} />;
|
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) {
|
if (this.props.scrollbar) {
|
||||||
return <AutoHideScrollbar className={classes}>{content}</AutoHideScrollbar>;
|
return <AutoHideScrollbar className={classes}>{content}</AutoHideScrollbar>;
|
||||||
|
|||||||
@@ -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.
|
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 { render, screen } from "jest-matrix-react";
|
||||||
import parse from "html-react-parser";
|
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 SettingsStore from "../../src/settings/SettingsStore";
|
||||||
import { getMockClientWithEventEmitter } from "../test-utils";
|
import { getMockClientWithEventEmitter } from "../test-utils";
|
||||||
import { SettingLevel } from "../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||||
@@ -319,3 +319,12 @@ describe("bodyToNode", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("sanitizedHtmlNode", () => {
|
||||||
|
it("should respect className", () => {
|
||||||
|
const sanitized = sanitizedHtmlNode('<a href="https://google.com">Link</a>', "testClass");
|
||||||
|
const { asFragment, getByText } = render(sanitized as ReactElement);
|
||||||
|
expect(getByText("Link").parentNode).toHaveClass("testClass");
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -91,3 +91,20 @@ exports[`bodyToNode should handle inline media when mediaIsVisible is true 1`] =
|
|||||||
</span>
|
</span>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`sanitizedHtmlNode should respect className 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="testClass"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://google.com"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Link
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -47,4 +47,13 @@ describe("<EmbeddedPage />", () => {
|
|||||||
const { asFragment } = render(<EmbeddedPage />);
|
const { asFragment } = render(<EmbeddedPage />);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should sanitise input", async () => {
|
||||||
|
fetchMock.get("https://other.page", `<h1>Foo</h1><iframe src="https://home.page" />`);
|
||||||
|
|
||||||
|
const { asFragment } = render(<EmbeddedPage url="https://other.page" />);
|
||||||
|
await expect(screen.findByText("Foo")).resolves.toBeVisible();
|
||||||
|
expect(screen.queryByRole("iframe")).not.toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+23
@@ -7,11 +7,29 @@ exports[`<EmbeddedPage /> should render nothing if no url given 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<EmbeddedPage /> should sanitise input 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="undefined_guest"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
Foo
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
@@ -19,6 +37,7 @@ exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
Couldn't load page
|
Couldn't load page
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +52,7 @@ exports[`<EmbeddedPage /> should translate _t strings ["] 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
Przeglądaj pokoje
|
Przeglądaj pokoje
|
||||||
@@ -49,6 +69,7 @@ exports[`<EmbeddedPage /> should translate _t strings [] 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
Przeglądaj pokoje
|
Przeglądaj pokoje
|
||||||
@@ -65,6 +86,7 @@ exports[`<EmbeddedPage /> should translate _t strings ["] 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
Przeglądaj pokoje
|
Przeglądaj pokoje
|
||||||
@@ -81,6 +103,7 @@ exports[`<EmbeddedPage /> should translate _t strings ['] 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="undefined_body"
|
class="undefined_body"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
Przeglądaj pokoje
|
Przeglądaj pokoje
|
||||||
|
|||||||
Reference in New Issue
Block a user