diff --git a/package-lock.json b/package-lock.json index 7874ac85ee..1b26c4cf80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "copy-to-clipboard": "^3.3.1", "design-token-editor": "^0.6.0", "django-cookie-consent": "^0.6.0", + "dompurify": "^3.2.4", "feelin": "^3.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", @@ -6328,6 +6329,13 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -8934,9 +8942,13 @@ } }, "node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/domutils": { "version": "2.8.0", @@ -9958,6 +9970,12 @@ "vanilla-picker": "^2.11.2" } }, + "node_modules/formiojs/node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/fraction.js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", @@ -22853,6 +22871,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -24867,9 +24891,12 @@ } }, "dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "requires": { + "@types/trusted-types": "^2.0.7" + } }, "domutils": { "version": "2.8.0", @@ -25661,6 +25688,13 @@ "tooltip.js": "^1.3.3", "uuid": "^8.3.2", "vanilla-picker": "^2.11.2" + }, + "dependencies": { + "dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==" + } } }, "fraction.js": { diff --git a/package.json b/package.json index 4a7e9072a2..58dcb01897 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "copy-to-clipboard": "^3.3.1", "design-token-editor": "^0.6.0", "django-cookie-consent": "^0.6.0", + "dompurify": "^3.2.4", "feelin": "^3.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", diff --git a/src/openforms/js/components/formio_builder/WebformBuilder.js b/src/openforms/js/components/formio_builder/WebformBuilder.js index 6c88edfefb..513e16b36d 100644 --- a/src/openforms/js/components/formio_builder/WebformBuilder.js +++ b/src/openforms/js/components/formio_builder/WebformBuilder.js @@ -10,12 +10,14 @@ import isEmpty from 'lodash/isEmpty'; import React from 'react'; import {createRoot} from 'react-dom/client'; import {IntlProvider} from 'react-intl'; +import sanitizeHtml from 'sanitize-html'; import {getIntlProviderProps} from 'components/admin/i18n'; import {getAvailableAuthPlugins} from 'components/form/cosign'; import {getAvailableDocumentTypes} from 'components/form/file'; import {getComponentEmptyValue} from 'components/utils'; import jsonScriptToVar from 'utils/json-script'; +import {sanitizeHTML} from 'utils/sanitize'; import {currentTheme} from 'utils/theme'; import { @@ -207,6 +209,22 @@ class WebformBuilder extends WebformBuilderFormio { })(); } + sanitizeComponentData(componentData) { + ['label', 'tooltip', 'description'].forEach(property => { + if (!componentData[property]) return; + componentData[property] = sanitizeHTML(componentData[property]); + }); + + // not-so-elegant input sanitation that formio does... FIXME: can't we just escape + // this with \" instead? + ['label', 'tooltip', 'placeholder'].forEach(property => { + if (!componentData[property]) return; + componentData[property] = componentData[property].replace(/"/g, "'"); + }); + + return componentData; + } + /** * saveComponent method when triggered from React events/formio builder. * @@ -229,13 +247,9 @@ class WebformBuilder extends WebformBuilderFormio { return NativePromise.resolve(); } - // not-so-elegant input sanitation that formio does... FIXME: can't we just escape - // this with \" instead? if (componentData) { - ['label', 'tooltip', 'placeholder'].forEach(property => { - if (!componentData[property]) return; - componentData[property] = componentData[property].replace(/"/g, "'"); - }); + // Perform some basic component data sanitizing + componentData = this.sanitizeComponentData(componentData); } // look up the component instance with the parent diff --git a/src/openforms/js/utils/sanitize.js b/src/openforms/js/utils/sanitize.js new file mode 100644 index 0000000000..3b6989ce23 --- /dev/null +++ b/src/openforms/js/utils/sanitize.js @@ -0,0 +1,35 @@ +import DOMPurify from 'dompurify'; + +const ALLOWED_HTML_TAGS = [ + // Basic text tags + 'a', + 'b', + 'br', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'i', + 'p', + 's', + 'strong', + 'sup', + 'u', + // Lists + 'li', + 'ol', + 'ul', +]; + +const ALLOWED_HTML_ATTRIBUTES = ['href', 'target', 'rel']; + +export const sanitizeHTML = data => { + return DOMPurify.sanitize(data, { + ALLOWED_TAGS: ALLOWED_HTML_TAGS, + ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES, + ALLOW_DATA_ATTR: true, + }); +};