导出的 HTML 可能保留不受信任元素的可执行属性和嵌入内容
原文依据:4 处克隆逻辑只明确删除注释和 SCRIPT 元素;其他元素通过 cloneNode(false) 复制原有属性,再递归复制子节点。它没有移除 onload/onclick 等事件属性,也没有排除 iframe、object、embed、form 等活动元素,随后把克隆结果直接写入独立 HTML。
如果画板包含恶意或被污染的 HTML,用户打开下载文件时可能执行其中的 JavaScript、加载外部页面或提交数据。该代码还可能读取导出页面内的内容并发送到网络。
风险有源码依据,但发生在用户点击“下载 HTML”并随后打开文件时。克隆器只排除注释和 `SCRIPT`,而 `cloneNode(false)` 会保留其他元素的属性;结果未经活动内容过滤便写入独立 HTML。因此,若画板含事件属性、iframe、object 等不受信任内容,导出文件可能保留其行为。用户可要求作者对允许的标签和属性采用白名单,并在隔离环境中打开导出文件。
const cloneStyled = (src) => { if (src.nodeType === 8 || (src.nodeType === 1 && src.tagName === 'SCRIPT')) return document.createTextNode(''); const dst = src.cloneNode(false); if (src.nodeType === 1) { const cs = getComputedStyle(src); let txt = ''; for (let i = 0; i < cs.length; i++) txt += cs[i] + ':' + cs.getPropertyValue(cs[i]) + ';'; dst.setAttribute('style', txt + 'animation:none;transition:none;'); if (src.tagName === 'CANVAS') try { const im = document.createElement('img'); im.src = src.toDataURL(); im.setAttribute('style', txt); return im; } catch {} } for (let c = src.firstChild; c; c = c.nextSibling) dst.appendChild(cloneStyled(c)); return dst; };查看另外 3 个位置
const xml = new XMLSerializer().serializeToString(clone); const save = (blob, ext) => { if (!blob) return; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name + '.' + ext; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }; if (kind === 'html') { const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + name + '</title>' + (fontCss ? '<style>' + fontCss + '</style>' : '') + '</head><body style="margin:0">' + xml + '</body></html>'; return save(new Blob([html], { type: 'text/html' }), 'html'); } if (kind === 'html') { const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + name + '</title>' + (fontCss ? '<style>' + fontCss + '</style>' : '') + '</head><body style="margin:0">' + xml + '</body></html>'; return save(new Blob([html], { type: 'text/html' }), 'html'); } </button> {menuOpen && ( <div className="dc-menu" onPointerDown={(e) => e.stopPropagation()}> <button onClick={() => doExport('png')}>下载 PNG</button> <button onClick={() => doExport('html')}>下载 HTML</button> <button className="dc-danger"