Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: sanitise sharedb node data using dompurify #2478

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"classnames": "^2.3.2",
"core-js": "^3.31.0",
"date-fns": "^2.30.0",
"dompurify": "^3.0.6",
"dotenv": "^16.3.1",
"formik": "^2.4.2",
"graphql": "^16.8.1",
Expand Down Expand Up @@ -116,6 +117,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/dompurify": "^3.0.5",
"@types/draft-js": "^0.11.12",
"@types/jest": "^27.5.2",
"@types/jest-axe": "^3.5.5",
Expand Down
22 changes: 19 additions & 3 deletions editor.planx.uk/pnpm-lock.yaml

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

27 changes: 27 additions & 0 deletions editor.planx.uk/src/@planx/graph/__tests__/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,33 @@ test("ignores empty values", () => {
]);
});

test("sanitizes unsafe input", () => {
const [graph, ops] = add({
id: "test",
type: 100,
data: {
text: "efef",
description: "<p>Test<img src=x onerror=prompt('Stored XSS')/></p>",
},
})({});
expect(graph).toEqual({
_root: {
edges: ["test"],
},
test: {
type: 100,
data: {
text: "efef",
description: `<p>Test<img src="x"></p>`,
},
},
});
expect(ops).toEqual([
{ oi: { edges: ["test"] }, p: ["_root"] },
{ oi: { data: { text: "efef", description: `<p>Test<img src="x"></p>` }, type: 100 }, p: ["test"] },
]);
});

test("empty graph", () => {
const [graph, ops] = add({ id: "a" })();
expect(graph).toEqual({
Expand Down
13 changes: 13 additions & 0 deletions editor.planx.uk/src/@planx/graph/__tests__/sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { sanitize } from "..";

test("sanitizes string", () => {
const bad = "<p>Test<img src=x onerror=prompt('Stored XSS')/></p>";
expect(sanitize(bad)).toEqual(`<p>Test<img src="x"></p>`);
});

test("sanitizes data object values", () => {
const badData = {
description: "<p>Test<img src=x onerror=prompt('Stored XSS')/></p>",
};
expect(sanitize(badData)).toEqual({ description: `<p>Test<img src="x"></p>` });
});
7 changes: 5 additions & 2 deletions editor.planx.uk/src/@planx/graph/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TYPES } from "@planx/components/types";
import DOMPurify from "dompurify";
import { enablePatches, produceWithPatches } from "immer";
import { isEqual } from "lodash";
import difference from "lodash/difference";
import isEqual from "lodash/isEqual";
import trim from "lodash/trim";
import zip from "lodash/zip";
import { customAlphabet } from "nanoid-good";
Expand Down Expand Up @@ -44,12 +45,14 @@ const isSectionNodeType = (id: string, graph: Graph): boolean =>
const isExternalPortalNodeType = (id: string, graph: Graph): boolean =>
graph[id]?.type === TYPES.ExternalPortal;

const sanitize = (x: any) => {
export const sanitize = (x: any) => {
if ((x && typeof x === "string") || x instanceof String) {
x = DOMPurify.sanitize(x as string);
return trim(x.replace(/[\u200B-\u200D\uFEFF↵]/g, ""));
} else if ((x && typeof x === "object") || x instanceof Object) {
return Object.entries(x).reduce((acc, [k, v]) => {
v = sanitize(v);
acc[k] = v;
Copy link
Member Author

@jessicamcinchak jessicamcinchak Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block only return acc, and v = sanitize(v) was indeed sanitizing, but not actually updating the acc which is returned:

  • In cases of handling empty values, this didn't matter because we delete acc[k]
  • But in cases of sanitising values we want to keep, we need to update the return value too

if (
!isSomething(v) ||
(typeof v === "object" && Object.keys(v as object).length === 0)
Expand Down
Loading