/** * Patch monaco-editor's bundled DOMPurify from 3.2.7 (vulnerable) * to 3.3.2 (patched) to fix XSS vulnerability. * * monaco-editor@0.55.1 hardcodes dompurify@3.2.7 as a bundled dependency. * This script replaces the bundled file with the patched version. * * Affected vulnerability: CVE in DOMPurify 3.1.3-3.3.1 (SAFE_FOR_XML bypass * via noscript, xmp, noembed, noframes, iframe rawtext elements). */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import https from "node:https"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, ".."); const MONACO_DOMPURIFY = join( ROOT, "node_modules", "monaco-editor", "esm", "vs", "base", "browser", "dompurify", "dompurify.js", ); // Files inside monaco-editor dev/min bundles that embed dompurify source const BUNDLED_FILES = [ join(ROOT, "node_modules", "monaco-editor", "dev", "vs", "editor.api.js"), join(ROOT, "node_modules", "monaco-editor", "min", "vs", "editor.api.min.js"), ]; const _PATCHED_VERSION = "3.3.3"; const PATCHED_URL = "https://raw.githubusercontent.com/cure53/DOMPurify/main/dist/purify.js"; const dompurifyVersion = (content) => { const m = content.match(/DOMPurify\s+(\d+\.\d+\.\d+)/); return m ? m[1] : null; }; const isVulnerable = (version) => { if (!version) return false; const [major, minor, patch] = version.split(".").map(Number); if (major !== 3) return false; if (minor === 1 && patch >= 3) return true; if (minor === 2) return true; if (minor === 3 && patch <= 1) return true; return false; }; const download = (url) => new Promise((resolve, reject) => { https .get(url, (res) => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; } let d = ""; res.on("data", (c) => (d += c)); res.on("end", () => resolve(d)); }) .on("error", reject); }); async function patchFile(source, target) { const content = existsSync(target) ? readFileSync(target, "utf8") : null; const currentVersion = content ? dompurifyVersion(content) : null; if (currentVersion && !isVulnerable(currentVersion)) { return; } const patched = await download(source); const patchedVer = dompurifyVersion(patched); if (!patchedVer || isVulnerable(patchedVer)) { throw new Error(`Downloaded DOMPurify version is still vulnerable: got ${patchedVer}`); } writeFileSync(target, patched, "utf8"); } async function patchBundledContent(target) { if (!existsSync(target)) return; const content = readFileSync(target, "utf8"); const currentVersion = dompurifyVersion(content); if (!currentVersion || !isVulnerable(currentVersion)) return; const patched = await download(PATCHED_URL); // Replace the dompurify version header + IIFE in the bundled file const patchedHeader = patched.match( /\/\*! @license DOMPurify [\d.]+ \|[\s\S]*?Mozilla Public License 2.0 \*\/[\s\S]*?^\(function\(\)/m, )?.[0]; if (patchedHeader) { const newContent = content.replace(/\/\*! @license DOMPurify 3\.2\.7[\s\S]*?^\(function\(\)/m, patchedHeader); writeFileSync(target, newContent, "utf8"); } else { console.warn(`[patch-dompurify] Could not find dompurify IIFE pattern in ${target}, skipping.`); } } async function main() { try { await patchFile(PATCHED_URL, MONACO_DOMPURIFY); for (const file of BUNDLED_FILES) { await patchBundledContent(file); } } catch (err) { // Don't fail the install console.error(`[patch-dompurify] ERROR: ${err.message}`); process.exit(0); } } main();