在浏览器中运行的付费墙或权益检查,只有在你让它变得如此的情况下,查找和打补丁的成本才会很高。压缩后的输出两者都做不到。
为什么付费墙判断只需一行补丁
在普通或压缩后的输出中,付费墙判断通常是一个返回布尔值的具名函数。任何阅读打包产物的人都可以搜索为它命名的那个词:isSubscribed、hasAccess、checkEntitlement。一旦找到,最廉价的攻击方式就是用一行补丁翻转返回值。
这个补丁在每个用户的文件副本上都有效,并且只要文件形态不变,它在你下一次发布时依然有效。查找判断逻辑的工作只需付出一次,之后每次构建都可以复用。
构建之后,已发布的文件是什么样子
默认构建会重命名标识符并编码字符串字面量,因此原本叫 isSubscribed 的函数不再带有那个名字,它周围的字符串也不再是可读文本。在已发布的文件中搜索那个名字或那条消息,什么补丁点都找不到。
这里最重要的属性是逐构建的多态性。种子在每次构建时都是新的,除非你固定它,因此针对某一次发布写的补丁不会对齐到下一次。一旦攻击者摸清判断逻辑所在的位置,你每次发布都会让他重新付出这个成本。
提高判断逻辑的保护强度
默认预设已经在较低的 complexity 目标下做了重命名和编码。要把整个打包产物提高一档:
$ npx afterpack@latest dist/ --preset=hard要只提高判断函数本身、不触碰打包产物的其余部分,用指令标记它:
/* @afterpack preset=hard */
function checkEntitlement(token: string): boolean {
return validateSubscription(token);
}
/* @afterpack end */检查判断逻辑是否得到了保护
构建之后打开Protection Map,找到判断函数。它的颜色应该比周围代码更深,反映出你为该区域设置的更高 complexity 目标。如果它看起来和打包产物的其余部分一样,读一下该次构建的诊断信息:被读取到但没有产生效果的指令会记录在那里。
客户端判断逻辑的边界在哪里
逐构建轮换才是这里真正持久有效的部分。每次发布都有新种子,意味着定位和修补判断逻辑的工作永远无法延续到下一次构建,因此针对某一次构建写的补丁、脚本或用户脚本,在下一次构建上就会失效。一个逐构建分析单次构建的静态读取者,依然可以还原出一段等价的计算,因为判断逻辑必须能在浏览器里正确运行,所以对于任何关系到收入的门槛,都要把客户端检查和服务端的权益校验搭配使用。
下一步
- 分区域混淆指令:完整的指令语法,以及还有哪些内容值得标记。
- 阻止用户脚本和扩展修补你的 Web 应用:结构性变化和新种子会对文本匹配式补丁造成什么影响。
- 威胁模型:静态读取者能还原什么,不能还原什么。