diff --git a/package-lock.json b/package-lock.json index 9256578de..abd1e1ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.31.4", + "version": "0.33.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.31.4", + "version": "0.33.0", "license": "GPL-3.0-or-later", "dependencies": { "@rgossiaux/svelte-headlessui": "^1.0.2", @@ -18,12 +18,14 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/dompurify": "^3.0.2", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", + "dompurify": "^3.0.5", "email-validator": "^2.0.4", "escape-html": "^1.0.3", "fake-dom": "^1.0.4", @@ -3799,6 +3801,14 @@ "@types/chai": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz", + "integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", @@ -3926,6 +3936,11 @@ "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz", "integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", + "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + }, "node_modules/@types/wikidata-sdk": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz", @@ -6009,10 +6024,9 @@ } }, "node_modules/dompurify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", - "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", - "optional": true + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==" }, "node_modules/domutils": { "version": "1.3.0", @@ -8394,6 +8408,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/jspdf/node_modules/dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==", + "optional": true + }, "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -16125,6 +16145,14 @@ "@types/chai": "*" } }, + "@types/dompurify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz", + "integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==", + "requires": { + "@types/trusted-types": "*" + } + }, "@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", @@ -16252,6 +16280,11 @@ "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz", "integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==" }, + "@types/trusted-types": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", + "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + }, "@types/wikidata-sdk": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz", @@ -17770,10 +17803,9 @@ } }, "dompurify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", - "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", - "optional": true + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==" }, "domutils": { "version": "1.3.0", @@ -19559,6 +19591,12 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz", "integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==", "optional": true + }, + "dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==", + "optional": true } } }, @@ -23212,4 +23250,4 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 534568b5d..0b8d6a20b 100644 --- a/package.json +++ b/package.json @@ -115,12 +115,14 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/dompurify": "^3.0.2", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", + "dompurify": "^3.0.5", "email-validator": "^2.0.4", "escape-html": "^1.0.3", "fake-dom": "^1.0.4", diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 778576654..4058b8c75 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -113,6 +113,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly floors: Store constructor(layout: LayoutConfig) { + Utils.initDomPurify() this.layout = layout this.featureSwitches = new FeatureSwitchState(layout) this.guistate = new MenuState( diff --git a/src/UI/Base/FromHtml.svelte b/src/UI/Base/FromHtml.svelte index b03b2624e..ee37d88bf 100644 --- a/src/UI/Base/FromHtml.svelte +++ b/src/UI/Base/FromHtml.svelte @@ -2,12 +2,18 @@ /** * Given an HTML string, properly shows this */ - + import DOMPurify from 'dompurify'; export let src: string + + let cleaned = DOMPurify.sanitize(src, { USE_PROFILES: { html: true }, + ADD_ATTR: ['target'] // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener' + }); + + let htmlElem: HTMLElement $: { if (htmlElem) { - htmlElem.innerHTML = src + htmlElem.innerHTML = cleaned } } diff --git a/src/Utils.ts b/src/Utils.ts index 9767027f4..cc09e8597 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,4 +1,5 @@ import colors from "./assets/colors.json" +import DOMPurify from "dompurify" export class Utils { /** @@ -25,6 +26,16 @@ Note that these values can be prepare with javascript in the theme by using a [c ` public static readonly imageExtensions = new Set(["jpg", "png", "svg", "jpeg", ".gif"]) + public static initDomPurify() { + DOMPurify.addHook("afterSanitizeAttributes", function (node) { + // set all elements owning target to target=_blank + add noopener noreferrer + if ("target" in node) { + node.setAttribute("target", "_blank") + node.setAttribute("rel", "noopener noreferrer") + } + }) + } + public static readonly special_visualizations_importRequirementDocs = `#### Importing a dataset into OpenStreetMap: requirements If you want to import a dataset, make sure that: