当前位置: 首页 > Web前端 > JavaScript

如何防止同事使用Evil.js代码投毒

时间:2023-03-27 16:19:07 JavaScript

视频移至B站https://www.bilibili.com/video...Evil.js最近讨论很多。项目介绍如下。npm被正式移除,代码也被关闭。作为一名前端老司机,我坚决反对这种行为。有很多方法可以发泄我个人的愤怒。代码中的毒药会被gitlog发现。犯法不如辞职今天就来讨论一下,如果你是项目负责人,如何识别这种代码中毒。欢迎加入前端学习。让我们一起成为国王,结交朋友。中毒法最简单,不可能中毒法就是直接替换函数。比如evil.js中json.stringify中毒,把里面的i换成了l。promise的then方法每周日有10%的概率不触发,只有周日才能触发有点亏,而且npm注册的叫lodash-utils,貌似是个正经的库,但它是中毒的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。默认的全局方法都是内置的,我们在命令行执行吧。我们可以简单粗略的查看函数的toStringfunctionisNative(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(){return`functionstringify(){[nativecode]}`}functionisNative(fn){returnfn.toString()===`function${fn.name}(){[nativecode]}`}console.log(isNative(JSON.stringify))//trueiframe我们也可以在浏览器中通过iframe创建一个隔离窗口,iframe加载到body之后,在iframe中获取contentWindowletiframe=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被污染让iframe=document.createElement('iframe')iframe.style.display='none'document.body.appendChild(iframe)let{JSON:cleanJSON}=iframe.contentWindowconsole.log(cleanJSON.stringify({name:'Illl'}))//'{"name":"llll"}'节点的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:'Ill'}))letsandbox={}vm.runInNewContext(`ret=JSON.stringify({name:'Ill'})`,sandbox)console.log(sandbox)ShadowRealmAPITC39有一个新的ShadowRealmapi,已经是stage3。您可以手动创建一个隔离的js运行环境。它被认为是下一代微前端的强大工具。不过现在兼容性不是很好,代码看起来有点像eval。然而,就像vm问题一样,我们需要指定一段代码来执行更多的ShadowRealm细节。可以参考何老的回答HowtoevaluateECMAScript'sShadowRealmAPIproposalconstsr=newShadowRealm()console.log(sr.evaluate(`JSON.stringify({name:'Illl'})`))Object.freeze我们可以也可以直接在项目代码的入口处使用Object.freeze冻结相关函数,保证不被修改,所以下面的代码会打印{"name":"Illl"},但是有些框架会做适当的修改原型链(比如Vue2中对数组的处理),修改stringify失败时我们没有任何提示,所以这种方式也要慎用,可能会导致你的项目出现bug!(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:'Illl'}))备份检测还有一个很简单的方法,实用性和兼容性适中。我们可以在项目启动之初备份一些重要的功能,比如Promise和Array原型链方法,JSON.stringify,fetch,localstorage.getItem等方法,必要时运行检测函数,判断Promise.prototype.then是否和我们备份的一样,就可以判断原型链是否被污染。我真的有点小聪明。首先,我们需要备份相关函数,因为我们不需要查很多,不需要遍历窗口,指定几个重要的api函数,都存储在_snapshots对象中//这段代码必须在项目开始执行!(global=>{constMSG='可能被篡改,小心'constinBrowser=typeofwindow!=='undefined'const{JSON:{parse,stringify},setTimeout,setInterval}=globallet_snapshots={JSON:{parse,stringify},setTimeout,setInterval,fetch}if(inBrowser){让{localStorage:{getItem,setItem},fetch}=global_snapshots.localStorage={getItem,setItem}_snapshots.fetch=fetch}})((0,eval)('this'))除了直接调用JSON、setTimeout外,原型链上还有Promise、Array等方法。我们可以通过getOwnPropertyNames获取,并备份到_protytypes,比如存储在Promise.prototype.then中,结果是//_protytypes{'Promise.then':functionthen(){[nativecode]}}!(global=>{let_protytypes={}constnames='Promise,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参数来直接恢复被污染的函数。嵌套,递归,直接暴力循环,欢迎有志之士优化){letobj=_snapshots[prop]//setTimeoutif(typeofobj==='function'){constisEqual=_snapshots[prop]===global[prop]if(!isEqual){console.log(`${prop}${MSG}`)if(reset){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的行为符合预期Summary好像没什么好总结的,祝你天天开心,做一个快乐的程序员,代码见StackBlitz的Github在线环境