Skip to content

Commit 6d11d59

Browse files
committed
Fix: Add concurrency lock in Reflection Widget sendMessage (#6171)
1 parent 6459fba commit 6d11d59

File tree

1 file changed

+73
-10
lines changed

1 file changed

+73
-10
lines changed

js/widgets/reflection.js

Lines changed: 73 additions & 10 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,8 +307,6 @@ 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 {
@@ -416,7 +413,6 @@ class ReflectionMatrix {
416413
*/
417414
async generateAnalysis() {
418415
try {
419-
console.log("Summary stored", this.summary);
420416
const response = await fetch(`${this.PORT}/analysis`, {
421417
method: "POST",
422418
headers: { "Content-Type": "application/json" },
@@ -575,8 +571,11 @@ class ReflectionMatrix {
575571
*/
576572
saveReport(data) {
577573
const key = "musicblocks_analysis";
578-
localStorage.setItem(key, data.response);
579-
console.log("Conversation saved in localStorage.");
574+
try {
575+
localStorage.setItem(key, data.response);
576+
} catch (e) {
577+
console.warn("Could not save analysis report to localStorage:", e);
578+
}
580579
}
581580

582581
/** Reads the analysis report from localStorage.
@@ -615,13 +614,76 @@ class ReflectionMatrix {
615614
URL.revokeObjectURL(url);
616615
}
617616

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

626688
// Headings
627689
html = html.replace(/^###### (.*$)/gim, "<h6>$1</h6>");
@@ -635,12 +697,13 @@ class ReflectionMatrix {
635697
html = html.replace(/\*\*(.*?)\*\*/gim, "<b>$1</b>");
636698
html = html.replace(/\*(.*?)\*/gim, "<i>$1</i>");
637699

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

641703
// Line breaks
642704
html = html.replace(/\n/gim, "<br>");
643705

644-
return html.trim();
706+
// Step 3: Sanitize the generated HTML using DOMParser
707+
return this.sanitizeHTML(html);
645708
}
646709
}

0 commit comments

Comments
 (0)