avaScript 是一种基于原型的面向对象语言。虽然你经常会看到class关键字,但它的底层本质还是用作原型。
在本文中,我们将了解 JavaScript 的原型性质,以及对象中的原型链。
首先检查以下代码:
const animals = {
name: "animal",
type: "object",
}
animals.hasOwnProperty("name")// true
但是我们并没有给animals定义方法hasOwnProperty,可它为什么可以调用该方法?在本文结束时,我们将了解其工作原理以及更多内容。
一、原型的概念
根据查阅得到的字面意思的解释是:
指一个词语或一个类型意义的所有典型模型或原形象,是一个类型的典型特征,我们可以用下图中的车辆示例很好地解释这一点。
原型“A”是创建其他版本(如“B”和“C”)的第一个版本。“A”包含车辆应具备的最基本功能,而“B”和“C”将包含更多的功能。
这意味着,“B”和“C”是“A”的改进版本,但仍包含“A”的特征。“A”有四个轮胎,“B”也有四个轮胎,但是可以飞,“C”同样有四个轮胎,但是可以在水上行驶。
JavaScript 在原型的基础上工作。在每个函数的声明中,JavaScript 引擎将prototype属性添加到该函数,这使该函数成为可以创建其他版本的原型。我们可以通过打印其属性确认:
function hello() {
console.log("hello")
}
console.dir(hello)
结果:
如上图所示,显示了函数的属性,hello函数中包括了prototype属性, 以及另一个名为__proto__的属性。本文稍后会详细介绍。
该prototype对象有两个属性:一个名为constructor以及另一个同样名为__proto__的属性。前者指向hello函数,后者指向Object。
二、原型的好处
说原型的好处之前我们先说一下构造函数,这是创建对象的一种方式,如下所示:
function Hello() {
console.log("hello")
}
const anotherVersion = new Hello()
anotherVersion.type = "new"
console.log(anotherVersion)
Hello首字母大写是一种约定,表示该函数可以用作构造对象,这个函数也被称为构造函数。
结果:
结果现在向我们展示了这个anotherVersion对象是一个从Hello函数通过new关键字而变化来的。你可以通过这种方式去创建类似具有相同功能的对象,例如:
function Obj(name) {
this.name = name;
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
const java = new Obj("java")
console.log(javascript)// Obj {name: 'javascript', printName: f}
console.log(java)// Obj {name: 'java', printName: f}
构造函数中的this变量指向构造函数new出来的实例化对象(在上面的代码中是javascript和java)。
我们可以看到,虽然javascript和java具有不同的名称值,但它们具有相同的功能代码。
使用原型的好处就是,你可以通过一个构造函数去创建很多具有相同功能的对象,并且这些对象都具有不同的名字。
还记得上面hello函数有两个属性:prototype和__proto__。prototype还有两个属性:constructor和__proto__。使用构造函数创建对象时,使用了prototype属性的constructor属性,让我们用下面的代码检查一下:
function Obj(name) {
this.name = name
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
console.log(javascript)
结果:
从上图中,你会看到__proto__属性连接到我们的构造函数Obj。
三、与原型共享功能
现在我们知道函数的prototype属性使该函数成为可用于创建其他对象的原型。
如果该prototype属性有其他属性呢?我们知道,JavaScript 对象可以在任何时候添加新的属性,让我们来看看:
function Obj(name) {
this.name = name
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
Obj.prototype.printType = function () {
console.log(this.type)
}
console.log(javascript)
结果:
如上图所示,__proto__属性现在有一个printType方法,但对象javascript本身没有printType方法。由上面所述结果我们可以知道,__proto__属性连接我们的构造函数,由于javascript在默认情况下可以访问__proto__属性中的构造函数,因此它也可以访问printType。因此,以下操作将正常工作:
javascript.printType()// undefined
javascript.type = "language"
javascript.printType()// language
JavaScript 是如何做到这一点的呢?首先它检查对象是否存在该方法,如果不存在,它检查__proto__属性。
四、原型链
我们看最后一张图片,你会注意到车辆B和C也有自己的原型,这意味着Obj用作原型的对象也继承了另一个原型的一些特性,这称为原型链。
这说明,一个对象可以是原型的新版本,同时也是另一个对象的原型。因此,当你尝试访问对象上的属性时,JavaScript引擎开始从对象自身中查找该属性,如果没有,它会继续检查__proto__,一直到没有__proto__或者找到该属性。如果找到最后,此属性不存在时,返回undefined。
五、总 结
回到第一个代码块:
const animals = {
name: "animal",
type: "object",
}
animals.hasOwnProperty("name")// true
到现在你应该清楚了,对吧?
当你的animals在控制台打印的时候,您会注意到它有一个__proto__指向Object的原型。并且,Object的原型具有hasOwnProperty属性。animals继承了该属性,这使得它可以使用该属性。
Object在 JavaScript 中有一个所有对象都能继承的原型。Function、String等构造函数也从Object继承了属性。
这也就是为什么string.toLowerCase()也可以直接使用的原因。构造函数的原型对象String具有所有这些属性,因此字符串可以使用它们。
在浏览某个论坛的时候,第一次看到了JavaScript原型链污染漏洞。当时非常的好奇,当时我一直以为js作为一种前端语言,就算存在漏洞也是针对前端,不会危害到后端,因此我以为这种漏洞危害应该不大。可当我看到他的漏洞危害还有可以执行任意命令的时候,发现可能我想到有点简单了。js也是可以用来做后端语言的。这篇文章就来认识一下这个漏洞。
既然漏洞名称是JavaScript原型链污染,那么首先就要先明白JavaScript原型链是什么。
正如我们所知,Javascrip的复杂类型都是对象类型(Object),而js不是一门完全面对对象编程的语言。那么对于对象编程来说要考虑对象的继承。
js实现继承的核心就是原型链。我理解的就是原型链的存在就是js中的继承机制,保证函数或对象中的方法,属性可以向下传递。
js使用了构造函数来创建对象,如下,我们可以通过构造函数来定义一个类:
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 生成实例
const p = new Person('zhangsan', 18);
可以看到这个类除了我们定义的两个属性以外,还有一个prototype的属性。prototype指向函数的原型对象,这是一个显式原型属性,只有函数才拥有该属性。
prototype也拥有两个属性:
constructor:指向原型的构造函数
prototype:指向了Object的原型
在prototype的属性中有一个_proto_,那么这个_proto_与prototype又有什么关系?
原型prototype是类的一个属性,而所有用类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的p对象,其天生就具有类Person的属性和方法。
我们可以通过Person.prototype来访问Person类的原型,但Person实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__登场了。
也就是类可以用prototype来访问类的原型,而实例化的对象可以用_proto_来访问对象所在类的prototype属性。
总结:
其实我们只要明白一点就可以了,JavaScript原型链是js中实现继承的核心,js的对象都会执行其它的原型,最后指向的原型为null。最后关于原型链再来总结一下
1)js是通过原型链来实现继承的。
2)所有类对象在实例化的时候将会拥有prototype中的属性和方法
3)类可以使用prototype来访问类的原型对象,而实例化对象可以通过_proto_来访问类的原型对象
let f = new Foo();
f.constructor === Foo;
f._proto_ === Foo.prototype
f._proto_ === Foo.prototype
Foo._proto_ === Function.prototype
在了解了原型链的相关知识以后,可以来看看竟然什么是原型链污染漏洞。
上面说过实例化对象的__proto__指向了类的prototype。那么,如果我们修改了实例化对象__proto__中的值,是不是就可以修改类中的值呢?是否可以影响所有和这个对象来自同一个类、父祖类的对象?
其实这就是原型链污染的原理。我们通过修改实例化对象的__proto__中的值,污染了类本体,进而影响所有和这个对象来自同一个类、父祖类的对象。
p神的博客上有一个这样的例子,用来说明原型链污染:
在实际的情况中,我们可能只能控制部分参数,那么我们怎么才能为__proto__赋值呢?
要为__proto__赋值就要求__proto__作为变量传进去并且作为键名,这种情况一般出现在下面的三种场景中:
下面借用p神文章中的一个例子来看看具体操作,原文链接为:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
以对象merge为例,我们想象一个简单的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__,是不是就可以原型链污染呢?
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__并不是一个key,自然也不会修改Object的原型。从下面的图中也可以看出,在o1中,参数b并没有出现在原型中。
那么,如何让__proto__被认为是一个键名呢?
我们将代码改成如下:
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)
可见,新建的o3对象,也存在b属性,说明Object已经被污染
这是因为,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
再来看看o1发现,b属性是定义在原型之中的。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
接下来用一个cve漏洞来具体再看一下这个漏洞。
3.4.0版本之前的jQuery存在一个原型污染漏洞CVE-2019-11358,PoC如下。
//代码如下,如果从前端接收一个json内容,传到后端。
//json内容:JSON.parse('{"__proto__": {"z": 123}}')
const json1 = ajax();
jQuery.extend(true, {}, JSON.parse(json1));
console.log( "test" in {} ); // true
jQuery.extend () 函数用于将一个或多个对象的内容合并到目标对象
$.extend( [deep ], target, object1 [, objectN ] )
参数 | 描述 |
deep | 可选。 Boolean类型 指示是否深度合并对象,默认为false。如果该值为true,且多个对象的某个同名属性也都是对象,则该"属性对象"的属性也将进行合并。 |
target | Object类型 目标对象,其他对象的成员属性将被附加到该对象上。 |
object1 | 可选。 Object类型 第一个被合并的对象。 |
objectN | 可选。 Object类型 第N个被合并的对象。 |
再来看看实际的代码是如何去写入的。
首先下载jQuery,这里下载的是3.3.0版本
https://github.com/jquery/jquery/tree/3.3.0
在src/core.js中文件中可以找到该extend函数。看过源码,可以发现该函数的正好符合上面说的合并数据的概念,那么来看看它到底会不会被污染?
我们来动态调试一下这个程序:
引入jQuery脚本,并设置断点,进行调试
首先根据第一个参数判断是否进行深度拷贝,然后进行第一次循环,取得参数为__proto__
第二次循环,在__proto__中去参数进行赋值
此时再看原型已经被污染了
js原型链污染可以说原理并不是太难懂,关键是实际中如何去利用。关于这个漏洞也是看了很多大神的文章,它们的思路真的太厉害了,我还有很多需要学习的,跟大家一起共勉。
由于本人水平有限,文章中可能会出现一些错误,欢迎各位大佬指正,感激不尽。如果有什么好的想法也欢迎交流,谢谢大家了~~
https://www.freebuf.com/articles/web/275619.html
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://xz.aliyun.com/t/7025
https://www.freebuf.com/articles/web/264966.html
作者:Notadmin
文章来源:FreeBuf
avascript的原型链图
各种对象的__proto__和函数的prototype 都阐明了
不能保证100% 正确(有问题直接注释或者私信偶) 但是互联网上原型链图比这个全的偶没见过(右上角的Number Date们需要改进)
可以改进但是 不能再简单了
有点晕吧 大部分人都会晕
所有网上 一堆 xxx.__proto__.__proto__.__proto__ 皆可由此图搞定
那些刁钻古怪的问题退化为孔乙己的茴字问题
死记硬背吧 先 要不改行
90%的前端或者js程序员或者老师们对Javascript懂得不比这个多 嘿嘿
给手机看的
但是这个图里的所有褐色单向箭头链就是Javascript的原型链(颜色标注对理解js原型链很关键)
原型链大部分时候是不可见的(__proto__在firefox nodejs中可见)那么图退化为
上二图三特点
1.所有对象都在原型链上
2.除了null 每个对象都有且唯一的__proto__原型对象
3.除了null, Object.prototype,其它对象的原型对象虽然不可通过.__proto__操作访问 但是通过原型链上某个构造器(函数)的prototype属性都可以访问到
js coder大多时候要面对的是
优雅了吧 :-()
所有javascript重度编码都是操作上面这个图的元素 但是你心里至少要有下图
如果连这图都嫌弃不好记 最简单的铁三角 javascript 99%的幻化都由此来
原型链的本质是嘛?以后分解 嘿嘿
*请认真填写需求信息,我们会在24小时内与您取得联系。