Skip to content

Commit

Permalink
[WC-1328]: sanitize html in HTML element (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
rahmanunver authored Nov 22, 2022
2 parents 2020efe + 8ee06e3 commit c04eb9b
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 5 deletions.
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/html-element-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/user-event": "^13.2.1",
"@types/big.js": "^6.0.2",
"@types/dompurify": "^2.4.0",
"@types/react": "^17.0.52",
"@types/react-dom": "^17.0.18",
"@types/react-test-renderer": "<18.0.0",
Expand All @@ -57,5 +58,8 @@
"react": "~17.0.2",
"react-dom": "~17.0.2",
"react-test-renderer": "~17.0.2"
},
"dependencies": {
"dompurify": "^2.4.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createElement, HTMLAttributes, ReactElement, ReactNode } from "react";
import { useSanitize } from "../utils/props-utils";

interface HTMLTagProps {
tagName: keyof JSX.IntrinsicElements;
Expand All @@ -8,10 +9,11 @@ interface HTMLTagProps {
}

export function HTMLTag(props: HTMLTagProps): ReactElement {
const sanitize = useSanitize();
const Tag = props.tagName;
const { unsafeHTML } = props;
if (unsafeHTML !== undefined) {
return <Tag {...props.attributes} dangerouslySetInnerHTML={{ __html: unsafeHTML }} />;
return <Tag {...props.attributes} dangerouslySetInnerHTML={{ __html: sanitize(unsafeHTML) }} />;
}

return <Tag {...props.attributes}>{props.children}</Tag>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ describe("HTMLTag", () => {
expect(asFragment()).toMatchSnapshot();
});

it("with innerHTML apply html sanitizing", () => {
const checkSapshot = (html: string): void => {
expect(
render(
<HTMLTag
tagName="div"
unsafeHTML={html}
attributes={{
className: "html-element-root my-class"
}}
>
{undefined}
</HTMLTag>
).asFragment()
).toMatchSnapshot();
};

checkSapshot("<p>Lorem ipsum <script>alert(1)</script></p>");
checkSapshot("<img src=x onerror=alert(1)>");
checkSapshot(`<b onmouseover=alert(‘XSS testing!‘)>ok</b>`);
checkSapshot("<a>123</a><option><style><img src=x onerror=alert(1)></style>");
});

it("fires events", () => {
const cbFn = jest.fn();
const { getByTestId } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,52 @@ exports[`HTMLTag renders correctly with innerHTML 1`] = `
</div>
</DocumentFragment>
`;

exports[`HTMLTag with innerHTML apply html sanitizing 1`] = `
<DocumentFragment>
<div
class="html-element-root my-class"
>
<p>
Lorem ipsum
</p>
</div>
</DocumentFragment>
`;

exports[`HTMLTag with innerHTML apply html sanitizing 2`] = `
<DocumentFragment>
<div
class="html-element-root my-class"
>
<img
src="x"
/>
</div>
</DocumentFragment>
`;

exports[`HTMLTag with innerHTML apply html sanitizing 3`] = `
<DocumentFragment>
<div
class="html-element-root my-class"
>
<b>
ok
</b>
</div>
</DocumentFragment>
`;

exports[`HTMLTag with innerHTML apply html sanitizing 4`] = `
<DocumentFragment>
<div
class="html-element-root my-class"
>
<a>
123
</a>
<option />
</div>
</DocumentFragment>
`;
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ObjectItem, ListActionValue, ActionValue, DynamicValue, ListExpressionValue, ListWidgetValue } from "mendix";
import { ActionValue, DynamicValue, ListActionValue, ListExpressionValue, ListWidgetValue, ObjectItem } from "mendix";
import { ReactNode } from "react";
import {
createAttributeResolver,
createEventResolver,
prepareAttributes,
prepareChildren,
prepareEvents,
prepareHtml,
prepareTag
prepareTag,
createSanitize
} from "../props-utils";
import { ReactNode } from "react";

describe("props-utils", () => {
describe("prepareTag", () => {
Expand Down Expand Up @@ -320,6 +321,23 @@ describe("props-utils", () => {
});
});
});

describe("sanitizeHtml", () => {
it("apply html sanitizing", () => {
const sanitize = createSanitize();
expect(
sanitize(`<img src="nonexistent.png" onerror="alert('This restaurant got voted worst in town!');" />`)
).toBe(`<img src="nonexistent.png">`);

expect(sanitize(`<div><script>destroyWebsite();</script></div>`)).toBe(`<div></div>`);

expect(sanitize(`<body onload=alert(‘something’)>`)).toBe("");

expect(sanitize(`<a href="javascript:alert('Don't laugh, this is not a joke!')">hello</a>`)).toBe(
`<a>hello</a>`
);
});
});
});

function createActionValue(): [ActionValue, jest.Mock] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CSSProperties, DOMAttributes, HTMLAttributes, ReactNode, SyntheticEvent } from "react";
import { CSSProperties, DOMAttributes, HTMLAttributes, ReactNode, SyntheticEvent, useState } from "react";
import { ObjectItem } from "mendix";
import DOMPurify from "dompurify";

import { AttributesType, EventsType, HTMLElementContainerProps, TagNameEnum } from "../../typings/HTMLElementProps";
import { convertInlineCssToReactStyle } from "./style-utils";
Expand Down Expand Up @@ -139,3 +140,15 @@ export type VoidElement = typeof voidElements[number];
export function isVoidElement(tag: unknown): tag is VoidElement {
return voidElements.includes(tag as VoidElement);
}

type Sanitize = (html: string) => string;

export function createSanitize(): Sanitize {
const purify = DOMPurify(window);
return html => purify.sanitize(html);
}

export function useSanitize(): ReturnType<typeof createSanitize> {
const [sanitize] = useState(createSanitize);
return sanitize;
}
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c04eb9b

Please sign in to comment.