diff --git a/browser-extensions/extension/src/js/content-scripts/content-script-parkrunner.js b/browser-extensions/extension/src/js/content-scripts/content-script-parkrunner.js
index 8b444206..591ea206 100644
--- a/browser-extensions/extension/src/js/content-scripts/content-script-parkrunner.js
+++ b/browser-extensions/extension/src/js/content-scripts/content-script-parkrunner.js
@@ -21,7 +21,9 @@ function get_table(id, caption) {
.replace("–", "")
.trim()
.replace(/\s+/g, " ");
- const normalisedExpectedCaption = (caption || "").trim().replace(/\s+/g, " ");
+ const normalisedExpectedCaption = (caption || "")
+ .trim()
+ .replace(/\s+/g, " ");
return normalisedPageCaption === normalisedExpectedCaption;
});
}
@@ -63,22 +65,27 @@ function parse_volunteer_table(result) {
}
function set_complete_progress_message(errors) {
- var messages = [
- 'Additional badges provided by Running Challenges',
- ];
+ const safe_link =
+ 'Additional badges provided by Running Challenges';
+ const messages = [safe_link];
$.each(errors, function (index, error_message) {
- messages.push(error_message);
+ messages.push(escape_html(error_message));
});
if (errors.length > 0) {
messages.push("Refresh the page to try again");
}
- set_progress_message(messages.join("
"));
+ set_progress_message_html(messages.join("
"));
}
function set_progress_message(progress_message) {
console.log("Progress: " + progress_message);
- // $("div[id=running_challenges_messages_div]").html($("div[id=running_challenges_messages_div]").html() + "
" + progress_message)
- $("div[id=running_challenges_messages_div]").html(progress_message);
+ $("div[id=running_challenges_messages_div]").html(
+ escape_html(progress_message),
+ );
+}
+
+function set_progress_message_html(safe_html) {
+ $("div[id=running_challenges_messages_div]").html(safe_html);
}
function parsePageAthleteInfo() {
@@ -261,8 +268,8 @@ function create_skeleton_elements(id_map) {
id_map["messages"],
);
running_challenges_message_spacer.after(running_challenges_messages_div);
- // Use the progress message function to se interval
- set_progress_message(
+ // Use the progress message function to set initial message
+ set_progress_message_html(
'Loading Running Challenges Badges',
);
diff --git a/browser-extensions/extension/src/js/lib/escape-html.js b/browser-extensions/extension/src/js/lib/escape-html.js
new file mode 100644
index 00000000..f2efd92a
--- /dev/null
+++ b/browser-extensions/extension/src/js/lib/escape-html.js
@@ -0,0 +1,17 @@
+function escape_html(unsafe) {
+ if (unsafe == null) return "";
+ const s = String(unsafe);
+ return s
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+if (typeof window !== "undefined") {
+ window.escape_html = escape_html;
+}
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = { escape_html };
+}
diff --git a/browser-extensions/extension/src/js/tests/test/test_escape_html.js b/browser-extensions/extension/src/js/tests/test/test_escape_html.js
new file mode 100644
index 00000000..fa8ff589
--- /dev/null
+++ b/browser-extensions/extension/src/js/tests/test/test_escape_html.js
@@ -0,0 +1,38 @@
+const assert = require("assert");
+const { escape_html } = require("../../lib/escape-html.js");
+
+describe("escape_html", function () {
+ it("escapes angle brackets", function () {
+ assert.strictEqual(
+ escape_html(""),
+ "<script>alert(1)</script>",
+ );
+ });
+
+ it("escapes ampersand and quotes", function () {
+ assert.strictEqual(
+ escape_html("\"foo\" & 'bar'"),
+ ""foo" & 'bar'",
+ );
+ });
+
+ it("returns empty string for null", function () {
+ assert.strictEqual(escape_html(null), "");
+ });
+
+ it("returns empty string for undefined", function () {
+ assert.strictEqual(escape_html(undefined), "");
+ });
+
+ it("converts non-strings to string then escapes", function () {
+ assert.strictEqual(escape_html(1), "1");
+ assert.strictEqual(escape_html("<"), "<");
+ });
+
+ it("leaves safe text unchanged", function () {
+ assert.strictEqual(
+ escape_html("Parsing Athlete Info"),
+ "Parsing Athlete Info",
+ );
+ });
+});
diff --git a/browser-extensions/extension/src/manifest/base.json b/browser-extensions/extension/src/manifest/base.json
index bda5138b..bb1a085a 100644
--- a/browser-extensions/extension/src/manifest/base.json
+++ b/browser-extensions/extension/src/manifest/base.json
@@ -54,6 +54,7 @@
"js/lib/i18n.js",
"js/lib/challenges.js",
"js/lib/challenges_ui.js",
+ "js/lib/escape-html.js",
"js/content-scripts/content-script-parkrunner.js"
],
"css": [
diff --git a/browser-extensions/extension/userscript/entry.js b/browser-extensions/extension/userscript/entry.js
index ccb46d58..7429d3ee 100644
--- a/browser-extensions/extension/userscript/entry.js
+++ b/browser-extensions/extension/userscript/entry.js
@@ -15,6 +15,7 @@ import "../src/js/lib/cache.js";
import "../src/js/lib/i18n.js";
import "../src/js/lib/challenges.js";
import "../src/js/lib/challenges_ui.js";
+import "../src/js/lib/escape-html.js";
import "../src/js/content-scripts/content-script-parkrunner.js";
// Ensure the main page initialiser runs once the DOM is ready.