fix(security): sandbox preview iframes to prevent XSS
Add sandbox='allow-scripts' to all preview iframes. This isolates user-executed code from the parent page's localStorage (auth tokens), cookies, and DOM. Switch from document.write() to srcdoc attribute since sandboxed iframes can't use document.write(). Addresses SEC-1 (critical) from security audit.
This commit is contained in:
@@ -216,18 +216,18 @@ export class LessonEngine {
|
|||||||
iframe.style.height = "100%";
|
iframe.style.height = "100%";
|
||||||
iframe.style.border = "none";
|
iframe.style.border = "none";
|
||||||
iframe.title = "Preview";
|
iframe.title = "Preview";
|
||||||
|
iframe.setAttribute("sandbox", "allow-scripts");
|
||||||
|
|
||||||
const container = document.getElementById(previewContainer || "preview-area");
|
const container = document.getElementById(previewContainer || "preview-area");
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.appendChild(iframe);
|
container.appendChild(iframe);
|
||||||
|
|
||||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
let html;
|
||||||
iframeDoc.open();
|
|
||||||
|
|
||||||
if (mode === "html" || mode === "playground") {
|
if (mode === "html" || mode === "playground") {
|
||||||
// For HTML/playground mode, user code IS the HTML content (may include <style> blocks)
|
// For HTML/playground mode, user code IS the HTML content (may include <style> blocks)
|
||||||
const userHtml = this.userCode || "";
|
const userHtml = this.userCode || "";
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -239,11 +239,11 @@ export class LessonEngine {
|
|||||||
${userHtml}
|
${userHtml}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else if (mode === "tailwind") {
|
} else if (mode === "tailwind") {
|
||||||
// For Tailwind mode, user code goes directly in HTML classes
|
// For Tailwind mode, user code goes directly in HTML classes
|
||||||
const htmlWithClasses = this.injectTailwindClasses(previewHTML, this.userCode);
|
const htmlWithClasses = this.injectTailwindClasses(previewHTML, this.userCode);
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -256,11 +256,11 @@ export class LessonEngine {
|
|||||||
${htmlWithClasses}
|
${htmlWithClasses}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else if (mode === "markdown") {
|
} else if (mode === "markdown") {
|
||||||
// For Markdown mode, parse user code to HTML
|
// For Markdown mode, parse user code to HTML
|
||||||
const renderedHtml = marked.parse(this.userCode || "");
|
const renderedHtml = marked.parse(this.userCode || "");
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -288,11 +288,11 @@ export class LessonEngine {
|
|||||||
${renderedHtml}
|
${renderedHtml}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Original CSS mode
|
// Original CSS mode
|
||||||
const userCssWithWrapper = this.getCompleteCss();
|
const userCssWithWrapper = this.getCompleteCss();
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -305,10 +305,10 @@ export class LessonEngine {
|
|||||||
${previewHTML}
|
${previewHTML}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframeDoc.close();
|
iframe.srcdoc = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
injectTailwindClasses(html, userClasses) {
|
injectTailwindClasses(html, userClasses) {
|
||||||
@@ -341,6 +341,7 @@ export class LessonEngine {
|
|||||||
iframe.style.height = "100%";
|
iframe.style.height = "100%";
|
||||||
iframe.style.border = "none";
|
iframe.style.border = "none";
|
||||||
iframe.title = "Expected Result";
|
iframe.title = "Expected Result";
|
||||||
|
iframe.setAttribute("sandbox", "allow-scripts");
|
||||||
|
|
||||||
const container = document.getElementById("preview-expected");
|
const container = document.getElementById("preview-expected");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -348,12 +349,11 @@ export class LessonEngine {
|
|||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.appendChild(iframe);
|
container.appendChild(iframe);
|
||||||
|
|
||||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
let html;
|
||||||
iframeDoc.open();
|
|
||||||
|
|
||||||
if (mode === "html" || mode === "playground") {
|
if (mode === "html" || mode === "playground") {
|
||||||
// For HTML/playground mode, solution code IS the HTML content
|
// For HTML/playground mode, solution code IS the HTML content
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -365,11 +365,11 @@ export class LessonEngine {
|
|||||||
${solutionCode}
|
${solutionCode}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else if (mode === "tailwind") {
|
} else if (mode === "tailwind") {
|
||||||
// For Tailwind mode, inject solution classes into HTML
|
// For Tailwind mode, inject solution classes into HTML
|
||||||
const htmlWithClasses = this.injectTailwindClasses(previewHTML, solutionCode);
|
const htmlWithClasses = this.injectTailwindClasses(previewHTML, solutionCode);
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -382,11 +382,11 @@ export class LessonEngine {
|
|||||||
${htmlWithClasses}
|
${htmlWithClasses}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else if (mode === "markdown") {
|
} else if (mode === "markdown") {
|
||||||
// For Markdown mode, parse solution to HTML
|
// For Markdown mode, parse solution to HTML
|
||||||
const renderedHtml = marked.parse(solutionCode || "");
|
const renderedHtml = marked.parse(solutionCode || "");
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -414,12 +414,12 @@ export class LessonEngine {
|
|||||||
${renderedHtml}
|
${renderedHtml}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
} else {
|
} else {
|
||||||
// CSS mode - wrap solution with prefix/suffix
|
// CSS mode - wrap solution with prefix/suffix
|
||||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||||
const solutionCss = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
const solutionCss = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
||||||
iframeDoc.write(`
|
html = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -432,10 +432,10 @@ export class LessonEngine {
|
|||||||
${previewHTML}
|
${previewHTML}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframeDoc.close();
|
iframe.srcdoc = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user