diff --git a/js/SaveInterface.js b/js/SaveInterface.js index 8fd7b6357e..424e558458 100644 --- a/js/SaveInterface.js +++ b/js/SaveInterface.js @@ -225,6 +225,34 @@ class SaveInterface { * @instance */ prepareHTML() { + const escapeHTML = value => { + if (value === null || value === undefined) return ""; + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); + }; + + const sanitizeImageURL = value => { + if (value === null || value === undefined) return ""; + const raw = String(value).trim(); + if (raw === "") return ""; + + const lower = raw.toLowerCase(); + if (lower.startsWith("data:")) { + // Only allow data:image/* to avoid data:text/html, etc. + return /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i.test(raw) ? raw : ""; + } + + if (lower.startsWith("http://") || lower.startsWith("https://")) return raw; + + // Allow relative URLs but reject other schemes. + if (/^[a-z][a-z0-9+.-]*:/i.test(raw)) return ""; + return raw; + }; + let file = this.htmlSaveTemplate; let description = _("No description provided"); if (this.activity.PlanetInterface !== undefined) { @@ -245,10 +273,13 @@ class SaveInterface { } file = file - .replace(new RegExp("{{ project_description }}", "g"), description) - .replace(new RegExp("{{ project_name }}", "g"), name) - .replace(new RegExp("{{ data }}", "g"), data) - .replace(new RegExp("{{ project_image }}", "g"), image); + .replace(new RegExp("{{ project_description }}", "g"), () => escapeHTML(description)) + .replace(new RegExp("{{ project_name }}", "g"), () => escapeHTML(name)) + // Always render project data as text, never as HTML. + .replace(new RegExp("{{ data }}", "g"), () => escapeHTML(data)) + .replace(new RegExp("{{ project_image }}", "g"), () => + escapeHTML(sanitizeImageURL(image)) + ); return file; } diff --git a/js/__tests__/SaveInterface.test.js b/js/__tests__/SaveInterface.test.js index 2c2b4eb48b..04206417c4 100644 --- a/js/__tests__/SaveInterface.test.js +++ b/js/__tests__/SaveInterface.test.js @@ -184,15 +184,9 @@ describe("save HTML methods", () => { }); it("should replace placeholders with actual project data", () => { - let file = activity.htmlSaveTemplate; - file = file - .replace( - /{{ project_description }}/g, - activity.PlanetInterface.getCurrentProjectDescription() - ) - .replace(/{{ project_name }}/g, activity.PlanetInterface.getCurrentProjectName()) - .replace(/{{ data }}/g, activity.prepareExport()) - .replace(/{{ project_image }}/g, activity.PlanetInterface.getCurrentProjectImage()); + instance.activity = activity; + instance.htmlSaveTemplate = activity.htmlSaveTemplate; + const file = instance.prepareHTML(); expect(file).toContain("
Mock Description
"); @@ -200,6 +194,31 @@ describe("save HTML methods", () => { expect(file).toContain("{{ project_description }}