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: "'