如何移除JavaScript默认对象与函数 按需启用特性构建安全沙箱
默认全禁用、按需启用的JS精简沙箱落地实现方案
你现在直接在当前上下文重写globalThis、覆盖eval的做法存在本质安全漏洞,JS的原型链、作用域链存在大量隐式引用路径,只要漏堵一个点,用户就能通过类似({}).constructor.constructor的方式拿到原生构造函数,绕过你所有的禁用逻辑,根本做不到完全的默认禁用。
核心思路
放弃在主运行上下文里手动删/覆盖原生特性的黑名单思路,直接创建完全独立的隔离执行上下文,初始状态下上下文不携带任何JS原生内置对象与宿主API,后续只手动往上下文里注入你明确要开放的能力,从根源实现默认全禁用、白名单按需开启的效果。
具体实现步骤
1. 选择合适的隔离边界,创建空白初始上下文
根据你的运行环境选对应的隔离方案,不要在主上下文做清理:
- Node.js环境:直接用内置的
vm模块创建隔离上下文,初始全局用无原型的空对象,从根源避免原型链污染
示例代码:
const vm = require('vm'); // 初始化完全空白的全局对象,无原型链,无任何默认属性 const sandboxGlobal = Object.create(null); // 全局指向自身,避免访问到外部全局 sandboxGlobal.globalThis = sandboxGlobal; // 👉 这里只手动添加你确定要开放的能力,比如自定义日志、固定的数学常量 sandboxGlobal.log = (...args) => { // 可以在这里加日志审计、内容过滤逻辑 console.log('[沙箱输出]', ...args); }; sandboxGlobal.Math = { PI: Math.PI, floor: Math.floor }; // 创建隔离上下文,从引擎层面关闭高风险代码生成能力 const context = vm.createContext(sandboxGlobal, { codeGeneration: { strings: false, // 直接禁用eval、Function构造函数等从字符串生成代码的能力,不需要手动重写 wasm: false // 直接禁用WebAssembly编译,堵死Wasm逃逸路径 } }); // 执行用户代码,加超时防止死循环 try { vm.runInContext(` // 用户代码区域 log(Math.floor(Math.PI)); // 正常输出3 // 访问eval、Reflect、WebAssembly、Object、Array全都是undefined `, context, { timeout: 1000, // 单段脚本执行超时时间1秒 displayErrors: true }); } catch (err) { console.error('沙箱执行异常:', err.message); }
- 浏览器环境:优先用标准的
ShadowRealmAPI创建独立域,兼容低版本浏览器可以用加了sandbox属性的iframe做隔离
ShadowRealm示例代码:
// 创建独立隔离域 const realm = new ShadowRealm(); // 清空域内默认挂载的所有原生全局属性 realm.evaluate(` const keepKeys = new Set(['globalThis']); for (const key of Object.getOwnPropertyNames(globalThis)) { if (!keepKeys.has(key)) { delete globalThis[key]; } } `); // 手动注入需要开放的能力 realm.evaluate(` globalThis.log = (text) => console.log('[沙箱]', text); `); // 执行用户代码 realm.evaluate(` log('测试执行'); // console.log(eval, Reflect, WebAssembly) // 全为undefined `);
如果用iframe方案,必须给iframe标签加sandbox="allow-scripts"属性,不要加allow-same-origin、allow-top-navigation等其他权限,保证iframe和主页面完全跨源隔离,加载完成后清空iframe contentWindow上的所有默认属性,再注入白名单能力即可。
2. 严格执行白名单规则,不要直接透传原生引用
所有注入沙箱的能力都要遵守几个原则:
- 不要直接把原生对象、原生函数透传到沙箱里,比如要开放数组能力,不要直接把全局
Array传进去,自己封装精简的实现,只暴露你需要的方法,剔除constructor、__proto__这类可以沿原型链逃逸的属性。 - 所有传入沙箱的回调函数,都要做参数校验、执行超时控制,避免通过回调逻辑拿到外部上下文引用。
- 除非明确需要,否则不要开放
Object、Reflect上的原型操作相关方法,比如getPrototypeOf、defineProperty、getOwnPropertyDescriptor这类API,这类API是沙箱逃逸最常用的入口。
3. 补全运行时防护
- 所有沙箱代码必须加执行超时限制,防止用户写死循环阻塞主进程,Node.js环境建议把沙箱跑在独立的
worker_threads里,超时直接终止线程,浏览器环境建议把沙箱跑在Web Worker里,避免阻塞主线程。 - 加内存占用限制,防止用户写恶意代码占满内存导致服务崩溃。
- 不要给沙箱开放任何宿主环境的敏感对象,比如Node.js里的
process、require,浏览器里的document、window、fetch这类API,如果要开放必须自己做一层严格的权限校验包装。
必须避开的坑
- 不要用黑名单思路做防护:你永远列不全所有高风险API,JS标准迭代会持续新增新的API,黑名单永远有漏网之鱼,白名单默认全关的模式才是长期可靠的。
- 不要在主上下文里做清理:主上下文的原生引用隐式路径极多,靠手动覆盖、删除属性永远清不干净,非常容易被逃逸。
- 不要依赖重写高危函数的方式做防护:比如把
eval重写为空函数,这类重写可以通过原型链拿到原生eval引用绕过,直接从引擎层面关闭代码生成能力才是可靠的。
内容的提问来源于stack exchange,提问作者Nightfall
相关产品推荐
相关产品推荐

