Skip to content

Commit 1bb48ab

Browse files
committed
Fix: Prevent projectAlgorithm overwrite in Reflection Widget (#6172)
1 parent 6459fba commit 1bb48ab

File tree

1 file changed

+73
-12
lines changed

1 file changed

+73
-12
lines changed

js/widgets/reflection.js

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ class ReflectionMatrix {
296296
async updateProjectCode() {
297297
const code = await this.activity.prepareExport();
298298
if (code === this.code) {
299-
console.log("No changes in code detected.");
300299
return; // No changes in code
301300
}
302301

@@ -308,15 +307,11 @@ class ReflectionMatrix {
308307
if (data.algorithm !== "unchanged") {
309308
this.projectAlgorithm = data.algorithm; // update algorithm
310309
this.code = code;
311-
} else {
312-
console.log("No changes in algorithm detected.");
313310
}
314311
this.botReplyDiv(data, false, false);
315312
} else {
316313
this.activity.errorMsg(_(data.error), 3000);
317314
}
318-
319-
this.projectAlgorithm = data.algorithm;
320315
}
321316

322317
/**
@@ -416,7 +411,6 @@ class ReflectionMatrix {
416411
*/
417412
async generateAnalysis() {
418413
try {
419-
console.log("Summary stored", this.summary);
420414
const response = await fetch(`${this.PORT}/analysis`, {
421415
method: "POST",
422416
headers: { "Content-Type": "application/json" },
@@ -575,8 +569,11 @@ class ReflectionMatrix {
575569
*/
576570
saveReport(data) {
577571
const key = "musicblocks_analysis";
578-
localStorage.setItem(key, data.response);
579-
console.log("Conversation saved in localStorage.");
572+
try {
573+
localStorage.setItem(key, data.response);
574+
} catch (e) {
575+
console.warn("Could not save analysis report to localStorage:", e);
576+
}
580577
}
581578

582579
/** Reads the analysis report from localStorage.
@@ -615,13 +612,76 @@ class ReflectionMatrix {
615612
URL.revokeObjectURL(url);
616613
}
617614

615+
/**
616+
* Escapes HTML special characters to prevent XSS attacks.
617+
* @param {string} text - The text to escape.
618+
* @returns {string} - The escaped text.
619+
*/
620+
escapeHTML(text) {
621+
const escapeMap = {
622+
"&": "&",
623+
"<": "&lt;",
624+
">": "&gt;",
625+
'"': "&quot;",
626+
"'": "&#x27;"
627+
};
628+
return text.replace(/[&<>"']/g, char => escapeMap[char]);
629+
}
630+
631+
/**
632+
* Sanitizes HTML content using DOMParser to prevent XSS.
633+
* Removes unsafe attributes and ensures links are safe.
634+
* @param {string} htmlString - The HTML string to sanitize.
635+
* @returns {string} - The sanitized HTML string.
636+
*/
637+
sanitizeHTML(htmlString) {
638+
const parser = new DOMParser();
639+
const doc = parser.parseFromString(htmlString, "text/html");
640+
641+
// Sanitize links
642+
const links = doc.getElementsByTagName("a");
643+
for (let i = 0; i < links.length; i++) {
644+
const link = links[i];
645+
const href = link.getAttribute("href");
646+
647+
// If no href, or it's unsafe, remove the attribute
648+
if (!href || this.isUnsafeUrl(href)) {
649+
link.removeAttribute("href");
650+
} else {
651+
// Enforce security attributes for external links
652+
link.setAttribute("target", "_blank");
653+
link.setAttribute("rel", "noopener noreferrer");
654+
}
655+
}
656+
657+
return doc.body.innerHTML;
658+
}
659+
660+
/**
661+
* Checks if a URL is unsafe (javascript:, data:, vbscript:).
662+
* @param {string} url - The URL to check.
663+
* @returns {boolean} - True if unsafe, false otherwise.
664+
*/
665+
isUnsafeUrl(url) {
666+
const trimmed = url.trim().toLowerCase();
667+
const unsafeSchemes = ["javascript:", "data:", "vbscript:"];
668+
// Check if it starts with any unsafe scheme
669+
// Note: DOMParser handles HTML entity decoding, so we check the raw attribute safely here
670+
// But for extra safety against control characters, we rely on the fact that
671+
// we are operating on the parsed DOM attribute.
672+
return unsafeSchemes.some(scheme => trimmed.replace(/\s+/g, "").startsWith(scheme));
673+
}
674+
618675
/**
619676
* Converts Markdown text to HTML.
620677
* @param {string} md - The Markdown text.
621678
* @returns {string} - The converted HTML text.
622679
*/
623680
mdToHTML(md) {
624-
let html = md;
681+
// Step 1: Escape HTML first to prevent XSS attacks from raw tags
682+
let html = this.escapeHTML(md);
683+
684+
// Step 2: Convert Markdown syntax to HTML
625685

626686
// Headings
627687
html = html.replace(/^###### (.*$)/gim, "<h6>$1</h6>");
@@ -635,12 +695,13 @@ class ReflectionMatrix {
635695
html = html.replace(/\*\*(.*?)\*\*/gim, "<b>$1</b>");
636696
html = html.replace(/\*(.*?)\*/gim, "<i>$1</i>");
637697

638-
// Links
639-
html = html.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank'>$1</a>");
698+
// Links - Create raw anchor tags, sanitization happens in Step 3
699+
html = html.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2'>$1</a>");
640700

641701
// Line breaks
642702
html = html.replace(/\n/gim, "<br>");
643703

644-
return html.trim();
704+
// Step 3: Sanitize the generated HTML using DOMParser
705+
return this.sanitizeHTML(html);
645706
}
646707
}

0 commit comments

Comments
 (0)