fix(dnfs): 前端代码节点改为隔离iframe沙箱运行,特效不再污染父页面导致运行后无法进入节点

This commit is contained in:
qing committed 2026-08-04 23:51:21 +08:00
1 parent 2ff7908bc7
commit f7ed1f785b
1 file changed
+25 -81
+25 -81
View File
@@ -492,12 +492,13 @@ function renderSpLink(container, sp, appendNote) {
appendNote(); appendNote();
} }
/* 前端代码节点:审计(展开源码,textContent 防注入)+ 运行(注入当前页面执行,支持完整 HTML/CSS/JS)。 /* 前端代码节点:审计(展开源码,textContent 防注入)+ 运行(在隔离 iframe 沙箱中执行完整 HTML/CSS/JS)。
* 不使用 new Function/eval(CSP 未开 unsafe-eval);解析完整 HTML 文档后注入当前页面: * 采用 iframe 沙箱而非注入父页面全局作用域,原因:
* - <style> 注入到页面 head * - 特效脚本运行在独立 iframe 内,无法访问父页面 DOM/JS,任何报错、死循环、变量覆盖
* - <script> 在当前页面全局作用域执行(可操作整页 DOM 加特效) * 都不会污染父页面,从而保证"运行特效后卡片点击/节点导航依然正常";
* - <body> 内容渲染到节点下方容器 * - iframe 全屏铺底且 pointer-events:none,不拦截父页面点击,视觉特效悬浮展示在页面上方;
* console.log/error/warn 输出捕获到下方输出区。仅管理员可发布(后端强制)。 */ * - 不使用 new Function/eval(CSP 未开 unsafe-eval),srcdoc 内联脚本由 CSP 'unsafe-inline' 放行。
* 仅管理员可发布(后端强制)。 */
function renderSpJs(container, sp, appendNote) { function renderSpJs(container, sp, appendNote) {
var bar = document.createElement('div'); var bar = document.createElement('div');
bar.className = 'sp-actions'; bar.className = 'sp-actions';
@@ -514,6 +515,7 @@ function renderSpJs(container, sp, appendNote) {
var out = document.createElement('div'); var out = document.createElement('div');
out.className = 'sp-run'; out.className = 'sp-run';
out.style.display = 'none'; out.style.display = 'none';
var frame = null; /* 当前特效 iframe 沙箱实例,null 表示未运行 */
audit_btn.addEventListener('click', function(e) { audit_btn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
var hidden = (pre.style.display === 'none'); var hidden = (pre.style.display === 'none');
@@ -522,83 +524,25 @@ function renderSpJs(container, sp, appendNote) {
}); });
run_btn.addEventListener('click', function(e) { run_btn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
out.style.display = out.style.display === 'none' ? 'block' : 'none'; /* 已运行:停止并移除特效沙箱 */
if (out.dataset.executed) return; // 已执行过,仅切换显示 if (frame) {
out.dataset.executed = '1'; frame.remove();
/* 本次执行专属输出缓冲:捕获脚本执行(含早期异步)期间的 console 输出,捕获窗口结束后恢复 frame = null;
* 应用自身 console,避免把应用日志(如 load_notes 打印的 pid/节点对象)刷进输出框干扰交互。 */ run_btn.textContent = '运行';
var lines = []; out.style.display = 'none';
var _orig = window.console;
var restore = function() {
window.console.log = _orig.log.bind(_orig);
window.console.error = _orig.error.bind(_orig);
window.console.warn = _orig.warn.bind(_orig);
};
window.console.log = function() {
lines.push(Array.prototype.slice.call(arguments).join(' '));
_orig.log.apply(_orig, arguments);
};
window.console.error = function() {
lines.push('Error: ' + Array.prototype.slice.call(arguments).join(' '));
_orig.error.apply(_orig, arguments);
};
window.console.warn = function() {
lines.push('Warn: ' + Array.prototype.slice.call(arguments).join(' '));
_orig.warn.apply(_orig, arguments);
};
var errHandler = function(ev) {
lines.push('Error: ' + ev.message);
};
window.addEventListener('error', errHandler);
/* 解析完整 HTML 文档 */
var doc;
try {
doc = new DOMParser().parseFromString(sp.code, 'text/html');
} catch (err) {
lines.push('Error: 解析 HTML 失败: ' + (err.message || err));
restore();
window.removeEventListener('error', errHandler);
out.textContent = lines.join('\n');
return; return;
} }
/* 注入 <style> 到页面 head */ /* 创建隔离 iframe 沙箱:完整前端代码(含 <style>/<script>/<body>)由此 srcdoc 承载,
Array.prototype.forEach.call(doc.querySelectorAll('style'), function(st) { * 脚本在独立上下文执行,无法影响父页面导航;pointer-events:none 保证不拦截父页面点击。 */
var s = document.createElement('style'); frame = document.createElement('iframe');
s.textContent = st.textContent; frame.className = 'sp-effect-frame';
document.head.appendChild(s); frame.setAttribute('sandbox', 'allow-scripts');
}); frame.srcdoc = sp.code;
/* 注入 <script>:包一层 IIFE 隔离变量作用域,避免污染页面全局(如覆盖 getNode/stack 等同名 frame.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;border:0;pointer-events:none;z-index:9999;';
* 变量导致卡片点击失效);函数体内仍可访问 document/window 对整页施加特效。 */ document.body.appendChild(frame);
Array.prototype.forEach.call(doc.querySelectorAll('script'), function(sc) { run_btn.textContent = '停止特效';
var s = document.createElement('script'); out.style.display = 'block';
if (sc.getAttribute('src')) { out.textContent = '特效已启动(运行于隔离沙箱,不影响页面导航)';
s.src = sc.getAttribute('src');
} else {
s.textContent = '(function(){\n' + sc.textContent + '\n})();';
}
document.head.appendChild(s);
});
/* 渲染 <body> 内容到节点下方容器(跳过 script,避免 appendChild 触发其重复执行) */
if (doc.body) {
Array.prototype.forEach.call(doc.body.childNodes, function(node) {
if (node.nodeName && node.nodeName.toLowerCase() === 'script') return;
out.appendChild(node);
});
}
/* console 输出区 */
var logBox = document.createElement('div');
logBox.className = 'sp-log';
out.appendChild(logBox);
var render = function() {
logBox.textContent = lines.join('\n') || '执行完成';
};
render();
/* 异步输出(setTimeout 等)轮询刷新;捕获窗口 3 秒后恢复应用自身 console,避免永久污染全局日志 */
setInterval(render, 400);
setTimeout(function() {
restore();
window.removeEventListener('error', errHandler);
}, 3000);
}); });
bar.appendChild(audit_btn); bar.appendChild(audit_btn);
bar.appendChild(run_btn); bar.appendChild(run_btn);