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.