Skip to content

Commit c04eb9b

Browse files
authored
[WC-1328]: sanitize html in HTML element (#165)
2 parents 2020efe + 8ee06e3 commit c04eb9b

File tree

7 files changed

+133
-5
lines changed

7 files changed

+133
-5
lines changed

packages/pluggableWidgets/html-element-web/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@testing-library/react-hooks": "^3.4.2",
4949
"@testing-library/user-event": "^13.2.1",
5050
"@types/big.js": "^6.0.2",
51+
"@types/dompurify": "^2.4.0",
5152
"@types/react": "^17.0.52",
5253
"@types/react-dom": "^17.0.18",
5354
"@types/react-test-renderer": "<18.0.0",
@@ -57,5 +58,8 @@
5758
"react": "~17.0.2",
5859
"react-dom": "~17.0.2",
5960
"react-test-renderer": "~17.0.2"
61+
},
62+
"dependencies": {
63+
"dompurify": "^2.4.1"
6064
}
6165
}

packages/pluggableWidgets/html-element-web/src/components/HTMLTag.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createElement, HTMLAttributes, ReactElement, ReactNode } from "react";
2+
import { useSanitize } from "../utils/props-utils";
23

34
interface HTMLTagProps {
45
tagName: keyof JSX.IntrinsicElements;
@@ -8,10 +9,11 @@ interface HTMLTagProps {
89
}
910

1011
export function HTMLTag(props: HTMLTagProps): ReactElement {
12+
const sanitize = useSanitize();
1113
const Tag = props.tagName;
1214
const { unsafeHTML } = props;
1315
if (unsafeHTML !== undefined) {
14-
return <Tag {...props.attributes} dangerouslySetInnerHTML={{ __html: unsafeHTML }} />;
16+
return <Tag {...props.attributes} dangerouslySetInnerHTML={{ __html: sanitize(unsafeHTML) }} />;
1517
}
1618

1719
return <Tag {...props.attributes}>{props.children}</Tag>;

packages/pluggableWidgets/html-element-web/src/components/__tests__/HTMLTag.spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,29 @@ describe("HTMLTag", () => {
3737
expect(asFragment()).toMatchSnapshot();
3838
});
3939

40+
it("with innerHTML apply html sanitizing", () => {
41+
const checkSapshot = (html: string): void => {
42+
expect(
43+
render(
44+
<HTMLTag
45+
tagName="div"
46+
unsafeHTML={html}
47+
attributes={{
48+
className: "html-element-root my-class"
49+
}}
50+
>
51+
{undefined}
52+
</HTMLTag>
53+
).asFragment()
54+
).toMatchSnapshot();
55+
};
56+
57+
checkSapshot("<p>Lorem ipsum <script>alert(1)</script></p>");
58+
checkSapshot("<img src=x onerror=alert(1)>");
59+
checkSapshot(`<b onmouseover=alert(‘XSS testing!‘)>ok</b>`);
60+
checkSapshot("<a>123</a><option><style><img src=x onerror=alert(1)></style>");
61+
});
62+
4063
it("fires events", () => {
4164
const cbFn = jest.fn();
4265
const { getByTestId } = render(

packages/pluggableWidgets/html-element-web/src/components/__tests__/__snapshots__/HTMLTag.spec.tsx.snap

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,52 @@ exports[`HTMLTag renders correctly with innerHTML 1`] = `
2424
</div>
2525
</DocumentFragment>
2626
`;
27+
28+
exports[`HTMLTag with innerHTML apply html sanitizing 1`] = `
29+
<DocumentFragment>
30+
<div
31+
class="html-element-root my-class"
32+
>
33+
<p>
34+
Lorem ipsum
35+
</p>
36+
</div>
37+
</DocumentFragment>
38+
`;
39+
40+
exports[`HTMLTag with innerHTML apply html sanitizing 2`] = `
41+
<DocumentFragment>
42+
<div
43+
class="html-element-root my-class"
44+
>
45+
<img
46+
src="x"
47+
/>
48+
</div>
49+
</DocumentFragment>
50+
`;
51+
52+
exports[`HTMLTag with innerHTML apply html sanitizing 3`] = `
53+
<DocumentFragment>
54+
<div
55+
class="html-element-root my-class"
56+
>
57+
<b>
58+
ok
59+
</b>
60+
</div>
61+
</DocumentFragment>
62+
`;
63+
64+
exports[`HTMLTag with innerHTML apply html sanitizing 4`] = `
65+
<DocumentFragment>
66+
<div
67+
class="html-element-root my-class"
68+
>
69+
<a>
70+
123
71+
</a>
72+
<option />
73+
</div>
74+
</DocumentFragment>
75+
`;

packages/pluggableWidgets/html-element-web/src/utils/__tests__/props-utils.spec.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { ObjectItem, ListActionValue, ActionValue, DynamicValue, ListExpressionValue, ListWidgetValue } from "mendix";
1+
import { ActionValue, DynamicValue, ListActionValue, ListExpressionValue, ListWidgetValue, ObjectItem } from "mendix";
2+
import { ReactNode } from "react";
23
import {
34
createAttributeResolver,
45
createEventResolver,
56
prepareAttributes,
67
prepareChildren,
78
prepareEvents,
89
prepareHtml,
9-
prepareTag
10+
prepareTag,
11+
createSanitize
1012
} from "../props-utils";
11-
import { ReactNode } from "react";
1213

1314
describe("props-utils", () => {
1415
describe("prepareTag", () => {
@@ -320,6 +321,23 @@ describe("props-utils", () => {
320321
});
321322
});
322323
});
324+
325+
describe("sanitizeHtml", () => {
326+
it("apply html sanitizing", () => {
327+
const sanitize = createSanitize();
328+
expect(
329+
sanitize(`<img src="nonexistent.png" onerror="alert('This restaurant got voted worst in town!');" />`)
330+
).toBe(`<img src="nonexistent.png">`);
331+
332+
expect(sanitize(`<div><script>destroyWebsite();</script></div>`)).toBe(`<div></div>`);
333+
334+
expect(sanitize(`<body onload=alert(‘something’)>`)).toBe("");
335+
336+
expect(sanitize(`<a href="javascript:alert('Don't laugh, this is not a joke!')">hello</a>`)).toBe(
337+
`<a>hello</a>`
338+
);
339+
});
340+
});
323341
});
324342

325343
function createActionValue(): [ActionValue, jest.Mock] {

packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { CSSProperties, DOMAttributes, HTMLAttributes, ReactNode, SyntheticEvent } from "react";
1+
import { CSSProperties, DOMAttributes, HTMLAttributes, ReactNode, SyntheticEvent, useState } from "react";
22
import { ObjectItem } from "mendix";
3+
import DOMPurify from "dompurify";
34

45
import { AttributesType, EventsType, HTMLElementContainerProps, TagNameEnum } from "../../typings/HTMLElementProps";
56
import { convertInlineCssToReactStyle } from "./style-utils";
@@ -139,3 +140,15 @@ export type VoidElement = typeof voidElements[number];
139140
export function isVoidElement(tag: unknown): tag is VoidElement {
140141
return voidElements.includes(tag as VoidElement);
141142
}
143+
144+
type Sanitize = (html: string) => string;
145+
146+
export function createSanitize(): Sanitize {
147+
const purify = DOMPurify(window);
148+
return html => purify.sanitize(html);
149+
}
150+
151+
export function useSanitize(): ReturnType<typeof createSanitize> {
152+
const [sanitize] = useState(createSanitize);
153+
return sanitize;
154+
}

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)