原文地址: yet another way to invoke functions without parentheses - bartosz's blog
本文并非原文的翻译,是经过我自己理解后的输出,聚焦我感兴趣或有价值的内容。对具体内容感兴趣的朋友可以去原文查看完整内容。
写过代码的小伙伴都知道,几乎大部分语言调用函数时都需要添加 ()。但原文作者却利用了 JavaScript 的语言特性巧妙地绕开了这一限制。答案就在下面几行代码中:
原型链污染 + V8 运行
眼尖的小伙伴看到代码里的 .prototype,就能猜到这里利用到了原型链。
是的,这个方法正是利用了原型链的污染。
原型链是 JavaScript 中非常重要的概念,所有的 Object 最终都会指向 Object.prototype。(想复习一下原型链的朋友,可以看一下我 8 年前的笔记 《JavaScript 高级程序设计》 读书笔记--从原型链复习继承)
了解原型链之后,让我们来分析一下具体代码。
Object.prototype.message = 'alert\x281337\x29'; // alert(1337)
Array.prototype.toString = Error.prototype.toString; // 覆盖 array.toString
Error.prepareStackTrace = Function; // 最关键的一步
const x = new Error;
x.valueOf = x.stack;
+x; // 会调用 alert(1337)
代码中的 Error.prepareStackTrace 是关键。
Error.prepareStackTrace 是什么?
在 V8 引擎中,Error.prototype.stack 是一个访问器属性。当你访问一个错误对象的 .stack 时,V8 会:
- 捕获当前的调用栈,生成一个由
CallSite对象组成的数组(称为 structuredStackTrace)。 - 检查是否存在
Error.prepareStackTrace函数。 - 如果存在,就调用它:
Error.prepareStackTrace(error, structuredStackTrace),并将返回值作为.stack的值。 - 如果不存在,则使用默认的格式化逻辑。
- 正常情况下,
Error.prepareStackTrace用于自定义堆栈字符串的格式,它接收两个参数: - error:错误对象本身。
- structuredStackTrace:堆栈帧数组。
当 Error.prepareStackTrace = Function 时发生了什么?
当 Error.prepareStackTrace 被替换成了 Function 构造函数。于是,在访问 x.stack 时,V8 实际上执行的是:
Function(error, structuredStackTrace)
Function 构造函数的行为是:最后一个参数作为函数体,前面的参数作为参数名列表。所以这里:
- 第一个参数
error会被转换成字符串,作为参数名。 - 第二个参数
structuredStackTrace会被转换成字符串,作为函数体。
注意,这里 structuredStackTrace 是堆栈帧数组。所以才会 Array.prototype.toString = Error.prototype.toString; 修改数组的 toString 方法。
error 对象的字符串化
既然利用的是 Error.prepareStackTrace 那自然要创建一个 error。于是便有了 const x = new Error;。
根据规范,Error.prototype.toString 会:
读取 this.name。对于 x,x.name 来自 Error.prototype.name,默认是 "Error"。
读取 this.message。Error.prototype 上已经有一个 message 属性,默认值是空字符串 ""。
所以 x.message 会先找到 Error.prototype.message,得到 "",而不会继续向上找到 Object.prototype.message。
structuredStackTrace 数组的字符串化
上一步提到 x.message 不会继续向上找到 Object.prototype.message。那这端的意义是什么呢?
Object.prototype.message = 'alert\x281337\x29'; // 即 'alert(1337)'
Array.prototype.toString = Error.prototype.toString;
structuredStackTrace 是一个数组。当 Function 构造函数需要将它转换为字符串时,会调用 structuredStackTrace.toString()。由于 Array.prototype.toString 已经被替换为 Error.prototype.toString,所以实际调用的是 Error.prototype.toString.call(structuredStackTrace)。
现在 this 是数组 structuredStackTrace:
- 读取
this.name。数组本身没有name,Array.prototype也没有name,Object.prototype也没有name。所以name是undefined。根据Error.prototype.toString的规则,undefined会被替换为 "Error"。 - 读取
this.message。数组本身没有message,Array.prototype没有message,但Object.prototype.message已经被我们设置为 'alert(1337)'。所以 msg 是 'alert(1337)'。 - 因为
name是 "Error",msg 是 'alert(1337)',所以返回 "Error: alert(1337)"。 - 因此,
Function的第二个参数(函数体)是字符串 "Error: alert(1337)"。
函数体 "Error: alert(1337)" 是合法的 JavaScript 代码:Error: 是一个标签(label),后面跟着表达式语句 alert(1337)。标签不影响执行,所以这个函数被调用时会执行 alert(1337)。
+x 进行触发
+x 会调用 x.valueOf(),也就是执行了 Error.stack 然后引发了上述的连锁反应。最终弹出 alert(1337)。
了解了有什么用呢?
从纯粹工程角度来看,这个代码不该出现在任何生产代码中(真写了也会被轰出去)。不过理解它,说明你对 JavaScript 中的原型链传递和防御检查有一个很深的认识。
在代码防御上,这类写法可以绕过静态的安全检查。
- 绕过WAF与XSS过滤器:许多WAF或输入过滤器会简单地拦截包含括号 () 的请求,以防止函数调用。
- 绕过ESLint的
no-new-func规则:ESLint 的no-new-func规则旨在禁止使用new Function(...)动态创建代码,以防范代码注入风险。 - 利用
Error.prepareStackTrace进行信息泄露与沙箱逃逸:Error.prepareStackTrace本身就是一个安全敏感点。真实的CVE(如 CVE-2026-92962)就出现在 vm2 这个沙箱库中。
而在基础概念上,也可以作为面试题考察知识深度。
- 看见“隐形”的原型链查找:当你写
obj.method()时,很少会去想引擎在原型链上爬了多少层。 - 理解“隐式转换”是一等公民:
+x触发valueOf是显式的知识,但Function(error, stackArray)内部对两个参数调用ToString才是这个攻击的灵魂。 - 认识“非标准钩子”的威力:
Error.prepareStackTrace是V8引擎的一个非标准扩展。
所以,它既是攻击技术,也是学习工具。
作为攻击技术,它的实战意义在于绕过脆弱的静态特征防御,这在XSS/WAF绕过和沙箱逃逸的特定场景中真实有效。
作为学习工具,它的价值在于将JavaScript中原型链的动态性和隐式转换的强制性从抽象概念变成了一个可运行的、有冲击力的具体案例。
欢迎关注同名公众号:此方的手账
不止技术。科技新闻解析、编程语言史、大厂史、世界简史正在连载中。
希望成为让你了解一些奇怪知识的电子榨菜和厕所读物。