|
|
|
|
@ -1,42 +1,8 @@ |
|
|
|
|
const ARTIFACT_CSP = [ |
|
|
|
|
"default-src 'none'", |
|
|
|
|
"script-src 'unsafe-inline'", |
|
|
|
|
"style-src 'unsafe-inline'", |
|
|
|
|
"img-src data: blob:", |
|
|
|
|
"font-src data:", |
|
|
|
|
"connect-src 'none'", |
|
|
|
|
"media-src 'none'", |
|
|
|
|
"frame-src 'none'", |
|
|
|
|
"object-src 'none'", |
|
|
|
|
"base-uri 'none'", |
|
|
|
|
"form-action 'none'", |
|
|
|
|
].join("; ") |
|
|
|
|
|
|
|
|
|
const DISALLOWED_SELECTORS = [ |
|
|
|
|
"iframe", |
|
|
|
|
"object", |
|
|
|
|
"embed", |
|
|
|
|
"form", |
|
|
|
|
"base", |
|
|
|
|
"meta[http-equiv='refresh' i]", |
|
|
|
|
"script[src]", |
|
|
|
|
"link[href]", |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
export function secureArtifactHtml(source) { |
|
|
|
|
const html = String(source || "").trim() |
|
|
|
|
if (!html || typeof DOMParser === "undefined") return "" |
|
|
|
|
|
|
|
|
|
const documentNode = new DOMParser().parseFromString(html, "text/html") |
|
|
|
|
documentNode.querySelectorAll(DISALLOWED_SELECTORS.join(",")).forEach((node) => node.remove()) |
|
|
|
|
documentNode.querySelectorAll("*").forEach((element) => sanitizeElement(element)) |
|
|
|
|
|
|
|
|
|
const previousCsp = documentNode.head.querySelector("meta[http-equiv='Content-Security-Policy' i]") |
|
|
|
|
previousCsp?.remove() |
|
|
|
|
const csp = documentNode.createElement("meta") |
|
|
|
|
csp.setAttribute("http-equiv", "Content-Security-Policy") |
|
|
|
|
csp.setAttribute("content", ARTIFACT_CSP) |
|
|
|
|
documentNode.head.prepend(csp) |
|
|
|
|
|
|
|
|
|
const monitor = documentNode.createElement("script") |
|
|
|
|
monitor.textContent = ` |
|
|
|
|
@ -49,22 +15,7 @@ export function secureArtifactHtml(source) { |
|
|
|
|
window.addEventListener("DOMContentLoaded", () => notify("ready")); |
|
|
|
|
})(); |
|
|
|
|
` |
|
|
|
|
csp.after(monitor) |
|
|
|
|
documentNode.head.prepend(monitor) |
|
|
|
|
|
|
|
|
|
return `<!doctype html>\n${documentNode.documentElement.outerHTML}` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function sanitizeElement(element) { |
|
|
|
|
Array.from(element.attributes || []).forEach((attribute) => { |
|
|
|
|
const name = attribute.name.toLowerCase() |
|
|
|
|
const value = String(attribute.value || "").trim() |
|
|
|
|
if (name.startsWith("on") || name === "target") { |
|
|
|
|
element.removeAttribute(attribute.name) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if (["href", "xlink:href", "src", "action", "formaction"].includes(name)) { |
|
|
|
|
const allowed = value.startsWith("#") || value.startsWith("data:") || value.startsWith("blob:") |
|
|
|
|
if (!allowed) element.removeAttribute(attribute.name) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|