• js原型链污染详解


    前言

    之前打某湖论剑,两道js的题,给我整懵逼了,发现以前都没对js做过多少研究,趁着被毒打了,先研究一波js原型链,未雨绸缪。

    基础

    protype

    首先我们研究js原型链,得搞明白原型是什么,这里借用p神的举的一个例子:
    在javascript中,我们定义一个类,需要以定义“构造函数”的方式来定义:

    function Foo() {
        this.bar = 1
    }
    
    new Foo()
    

    Foo()函数的内容就是构造函数的内容,this.bar是Foo的一个属性,学过c++的应该很容易理解,而且后面对原型链的利用也可以仿造c++类的思想来理解。

    一个类必然有方法,我们可以在构造函数里定义方法:

    function Foo() {
        this.bar = 1
        this.show = function() {
            console.log(this.bar)
        }
    }
    

    在js里,这样定义有一个特点,就是这个方法并不是绑定在类上,而是每创建一个对象,这个方法就会定义一次,这样就很浪费资源,我们要让它只定义一次,就需要用到原型prototype,这个prototype可以认为是一个类的属性,通过类创建的对象都将"继承"prototype的内容。

    function Foo() {
        this.bar = 1
    }
    
    Foo.prototype.show = function show() {
        console.log(this.bar)
    }
    

    __proto__

    那么__proto__是干什么用的?原来由类实例化的对象是无法直接访问到类的原型也就是prototype,我们可以看到foo没有prototype

    image

    这时__proto__就是对象访问类原型的媒介了

    image

    以上大概就是原型的内容,读者把原型的概念弄懂后再去看原型链污染会有不一样的结果

    原型链继承

    以上讲的prototype在js里其实主要是用来实现继承机制,这个继承跟c++的继承不太一样,js的继承可以改掉Object类导致原型链污染,而c++的继承是可以对父类的方法进行进行重写或重载,但不会直接就把父类给改写了,因为这个特性,这才有了原型链污染的诞生。这里举个例子:

    function Father() {
        this.first_name = 'Donald'
        this.last_name = 'Trump'
    }
    
    function Son() {
        this.first_name = 'Melania'
    }
    
    Son.prototype = new Father()
    
    let son = new Son()
    console.log(`Name: ${son.first_name} ${son.last_name}`)
    

    这里Son继承了Father的last_name

    image

    这里通过__proto__给Object添了个last_name

    image

    这里推导一下:
    son.__proto__ == Son.prototype
    son.__proto__.__proto__ == Father.prototype
    son.__proto__.__proto__.__proto__ == Object
    所以后面我们new的a,它有一个last_name是继承Object的,这里有趣的是,改写了Object后,Father.last__name == john,但是son.last_name == Trump,直接看Father函数,其实它的last_name并没有变化

    image

    这就有趣了,但重新new Father,它的last_name其实也是没变的

    image

    但我们还是成功污染了的,Object多了个last_name属性,此后新建的类都将继承这个属性,在某些地方是会造成危害的

    原型链污染实例

    在什么地方我们可以使用原型链污染?其实当有我们可以控制的"键名",并存在有赋值修改操作的地方,我们可以实现这个操作,这里举个js的merge函数经典例子:

    function merge(target, source) {
        for (let key in source) {
            if (key in source && key in target) {
                merge(target[key], source[key])
            } else {
                target[key] = source[key]
            }
        }
    }
    

    以上是一个简单的合并函数,我们可以看到target[key] = source[key],其实这个地方就存在原型链污染,如果key是__proto__的话,是不是就能修改到Object,这里给个例子:

    let o1 = {}
    let o2 = {a: 1, "__proto__": {b: 2}}
    merge(o1, o2)
    console.log(o1.a, o1.b)
    
    o3 = {}
    console.log(o3.b)
    

    image

    这里我们合并成功了,但是并没有污染到Object,原因是__proto__没有被当成键名,而是当成原型,也就是遍历键名的时候只有a,b是键名。

    为了让__proto__也被当成键名,我们可以把o2的值设置成json的格式,遍历json的时候,__proto__就会被当成键名,从而改写Object,例子如下:

    let o1 = {}
    let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
    merge(o1, o2)
    console.log(o1.a, o1.b)
    
    o3 = {}
    console.log(o3.b)
    

    成功改写Object,导致o3有了b属性

    image

    总结

    以上就是js原型链的基础内容,把基础打好,之后遇见原型链的题将会成长很快。


    __EOF__

  • 本文作者: F12
  • 本文链接: https://www.cnblogs.com/F12-blog/p/17141216.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    RK3568百兆网口
    (二十)ATP应用测试平台——websocket实现微服务版在线客服聊天室实战案例
    gopacket源码分析
    【React Hooks原理 - useRef】
    网关 GateWay 的使用详解、路由、过滤器、跨域配置
    消息中间件篇之RabbitMQ-高可用机制
    ⑩⑦【MySQL】锁:全局锁、表级锁、行级锁
    EvilAppleJuice(邪恶苹果汁)-ESP32C3项目(iphone疯狂弹窗)
    有关 python 切片的趣事
    【Android】PopupWindow焦点控制方式解析
  • 原文地址:https://www.cnblogs.com/F12-blog/p/17141216.html