如何安全执行含预定义对象与函数的用户JavaScript代码?
解决方案:安全且非阻塞的用户脚本执行方案
针对你的移动应用构建器面临的主线程阻塞和Web端恶意脚本风险问题,结合无法向隔离线程传递函数的限制,推荐采用消息代理模式分平台实现:
核心逻辑
由于postMessage仅支持可序列化数据,我们不在隔离线程中直接持有showAlert、haptics这类原生对象/函数,而是在隔离环境中创建代理对象/函数——这些代理通过消息向主线程发送执行指令,主线程完成实际操作后将结果回传,以此绕过函数传递的限制,同时实现权限管控。
React Native 端:Worklets + 消息回调
Worklets运行在UI线程,天然不会阻塞JS主线程,完美解决第一个问题:
在Worklet中定义代理函数
为每个预定义函数创建Worklet内的代理,通过sendToJS向主线程发送指令:// Worklet内部的代理函数 const showAlert = (title: string | undefined, message: string, actions: AlertAction[]) => { 'worklet'; sendToJS({ type: 'invoke', funcName: 'showAlert', payload: { title, message, actions } }); }; // 只读对象直接序列化传入(无安全风险) const currentUser = JSON.parse($args.currentUser);主线程监听并执行实际逻辑
主线程接收Worklet的消息,调用原生实现并处理结果:// 主线程启动Worklet const userScriptWorklet = runOnUI((serializedArgs) => { 'worklet'; const args = JSON.parse(serializedArgs); // 这里执行用户的脚本逻辑,调用的是代理函数 eval(args.formulaString); })(JSON.stringify({ formulaString: userInputCode, currentUser: JSON.stringify(this.currentUser) // 其他只读数据同理 })); // 监听Worklet的指令 userScriptWorklet.addListener((event) => { if (event.type === 'invoke') { const { funcName, payload } = event; switch(funcName) { case 'showAlert': this.showAlert(payload.title, payload.message, payload.actions); break; case 'openPage': this.openPage(payload.pageId); break; // 其他预定义函数逐一映射 } } });
Web 端:Web Worker/Iframe 沙箱 + 消息代理
Web Worker解决主线程阻塞问题,配合iframe沙箱彻底隔离恶意脚本的访问权限:
1. 沙箱隔离配置
- 若用Web Worker:Worker本身与主线程隔离,默认无法访问DOM、Cookie等敏感数据,只需避免暴露全局对象。
- 若用Iframe:设置严格的沙箱属性,彻底阻断恶意脚本的跨域/权限访问:
不要添加<iframe id="scriptSandbox" sandbox="allow-scripts" style="display: none;"></iframe>allow-same-origin,防止脚本读取父页面的Cookie、localStorage。
2. 代理函数与消息交互
在隔离环境(Worker/Iframe)中预定义代理,通过postMessage与主线程通信:
// Worker/Iframe内的全局代理实现 self.showAlert = async (title, message, actions) => { return new Promise((resolve) => { const reqId = Math.random().toString(36).slice(2); // 发送执行请求 self.postMessage({ type: 'invoke', id: reqId, funcName: 'showAlert', payload: { title, message, actions } }); // 监听结果回传 const handler = (e) => { if (e.data.id === reqId) { resolve(e.data.result); self.removeEventListener('message', handler); } }; self.addEventListener('message', handler); }); }; // 接收主线程传入的只读数据 self.addEventListener('message', (e) => { if (e.data.type === 'init') { self.currentUser = JSON.parse(e.data.payload.currentUser); // 初始化完成后执行用户脚本 eval(e.data.payload.formulaString); } });
3. 主线程消息处理
主线程仅响应预定义的函数调用,严格控制权限:
// 主线程初始化隔离环境并监听消息 const worker = new Worker('script-sandbox.js'); // 或获取iframe的contentWindow // const worker = document.getElementById('scriptSandbox').contentWindow; worker.postMessage({ type: 'init', payload: { formulaString: userInputCode, currentUser: JSON.stringify(currentUser) } }); worker.addEventListener('message', async (e) => { if (e.data.type === 'invoke') { const { id, funcName, payload } = e.data; let result; // 仅允许预定义的函数调用 switch(funcName) { case 'showAlert': result = await showAlert(payload.title, payload.message, payload.actions); break; case 'shareSheet': result = await shareSheet.open(payload.content); break; // 拒绝未授权的函数调用 default: result = new Error('Unauthorized function call'); } // 回传执行结果 worker.postMessage({ type: 'result', id, result }); } });
代码补全兼容性保障
确保隔离环境中的代理函数/对象结构与Monaco Editor中预定义的接口完全一致,比如Monaco中声明的showAlert参数、返回值,要和代理函数的定义严格匹配,这样用户输入的TS代码在转译后能无缝适配代理逻辑。
内容的提问来源于stack exchange,提问作者newbeeinthehouse
相关产品推荐
相关产品推荐

