diff --git a/app/assets/javascripts/discourse-common/addon/lib/escape.js b/app/assets/javascripts/discourse-common/addon/lib/escape.js new file mode 100644 index 00000000000..6b7da6a67f0 --- /dev/null +++ b/app/assets/javascripts/discourse-common/addon/lib/escape.js @@ -0,0 +1,32 @@ +const ESCAPE_REPLACEMENTS = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`", +}; +const BAD_CHARS = /[&<>"'`]/g; +const POSSIBLE_CHARS = /[&<>"'`]/; + +function escapeChar(chr) { + return ESCAPE_REPLACEMENTS[chr]; +} + +export default function escape(string) { + if (string === null) { + return ""; + } else if (!string) { + return string + ""; + } + + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if (!POSSIBLE_CHARS.test(string)) { + return string; + } + return string.replace(BAD_CHARS, escapeChar); +} diff --git a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js index 5f26ea7708a..8a01d633f02 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/icon-library.js +++ b/app/assets/javascripts/discourse-common/addon/lib/icon-library.js @@ -2,6 +2,7 @@ import I18n from "I18n"; import attributeHook from "discourse-common/lib/attribute-hook"; import { h } from "virtual-dom"; import { isDevelopment } from "discourse-common/config/environment"; +import escape from "discourse-common/lib/escape"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; let _renderers = []; @@ -140,25 +141,24 @@ registerIconRenderer({ name: "font-awesome", string(icon, params) { - const id = handleIconId(icon); - let html = ``; if (params.label) { - html += `${params.label}`; + html += `${escape(params.label)}`; } if (params.title) { - html = `${html}`; + html = `${html}`; } if (params.translatedtitle) { - html = `${html}`; } return html; @@ -176,7 +176,10 @@ registerIconRenderer({ }, [ h("use", { - "xlink:href": attributeHook("http://www.w3.org/1999/xlink", `#${id}`), + "xlink:href": attributeHook( + "http://www.w3.org/1999/xlink", + `#${escape(id)}` + ), namespace: SVG_NAMESPACE, }), ] diff --git a/app/assets/javascripts/discourse/tests/unit/lib/icon-library-test.js b/app/assets/javascripts/discourse/tests/unit/lib/icon-library-test.js index 3089ebec8fa..de03fa9c142 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/icon-library-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/icon-library-test.js @@ -24,4 +24,16 @@ module("Unit | Utility | icon-library", function () { const iconC = convertIconClass(" fab fa-facebook "); assert.ok(iconHTML(iconC).indexOf(" ") === -1, "trims whitespace"); }); + + test("escape icon names, classes and titles", function (assert) { + const html = iconHTML("'", { + translatedtitle: "'