JavaScript沙箱通过作用域限制和闭包机制实现,核心是用IIFE创建隔离作用域,并结合白名单上下文、Proxy拦截与全局API禁用,彻底切断不可信代码访问全局对象的隐式路径。

用作用域限制和闭包机制实现 JavaScript 沙箱,核心是让不可信代码只能访问你明确放行的变量和方法,完全隔绝全局对象(如 window、fetch、localStorage)——这不是靠“删掉”它们,而是让它根本“看不见”。闭包在这里不是炫技,而是天然的权限闸门。
用 IIFE 构建干净的私有作用域
最基础也最有效的隔离方式。IIFE(立即执行函数)创建一个新函数作用域,内部声明的变量、函数不会泄漏到外部,外部也无法直接访问内部内容。
- 写法示例:
(function() { const secret = 'only here'; console.log(secret); })();——secret在括号外完全不可见 - 把待执行的用户代码包裹进去:
(function(userCode) { eval(userCode); })('console.log("safe");');,此时eval运行在受限作用域内,无法读取或修改外部变量 - 注意:仅靠 IIFE 不足以阻止访问
window等全局对象,必须配合其他手段切断其引用路径
用闭包封装白名单上下文
闭包能“记住”它被定义时的词法环境。你可以预先构造一个只含安全 API 的对象,再通过闭包把它“锁进”执行函数里,用户代码只能通过这个窄通道调用。
- 例如:
const safeContext = { Math, JSON, parseInt, console: { log: (...args) => console.log('[sandbox]', ...args) } }; - 然后用
Function构造器动态生成函数,并显式绑定该上下文:const fn = new Function('context', 'with(context) { ' + userCode + ' }'); fn(safeContext); - 这样用户代码里写的
JSON.parse或console.log实际调用的是你提供的副本,而不是原始全局对象 - 关键点:不要把
window、self、this或原型链(如Object.prototype)暴露进去,否则可能被绕过
拦截并重写危险能力
即使有了闭包作用域,某些原生行为仍可能逃逸——比如 eval、Function 构造器、setTimeout 回调里的 this,都可能重新拿到全局环境。
立即学习“Java免费学习笔记(深入)”;
- 在上下文中主动覆盖:
safeContext.eval = () => { throw new Error('eval disabled'); }; - 重写异步方法,确保回调仍在沙箱上下文中执行:
safeContext.setTimeout = (fn, delay) => setTimeout(() => fn.call(safeContext), delay); - 禁用
with语句(它会污染作用域链),或至少不在沙箱中使用它——虽然上面示例用了,但生产环境更推荐用显式属性访问替代 - 对
Function构造器也要做同样处理,避免用户用new Function('return this;')()拿到全局对象
配合 Proxy 做细粒度访问控制
闭包提供“入口”,Proxy 提供“安检”。你可以用 Proxy 包裹白名单对象,在每次属性读取、方法调用时做检查,甚至动态拒绝或记录行为。
- 例如:
const proxyContext = new Proxy(safeContext, { get(target, prop) { if (prop === 'fetch') throw new Error('fetch not allowed'); return target[prop]; } }); - 这样即使用户写了
fetch,也会在取值阶段就被拦截,而不是等到执行时报错 - 还能做日志审计:
console.log(`Accessed ${prop}`),方便监控异常行为 - 注意 Proxy 本身不能防止原型链逃逸,所以要冻结目标对象或清理
__proto__和constructor
不复杂但容易忽略:闭包和作用域只是起点,真正的沙箱安全取决于你是否切断了所有通往全局环境的隐式路径。光有“私有变量”不够,关键是你没给它留门——包括原型链、构造器、this 绑定、隐式全局查找这些“后门”。


















