网安小白在学习原型链污染时一头雾水,参考了大佬们的文章,在这里记录一下学习笔记,方便日后复习,若有错误还请指正
JS中的对象(Object)是一种复合数据类型,它是一种无序的键值对集合。对象用于存储和传递多个值,每个值都有一个键(key)与之关联。
几种常见的对象的创建方式
第一种: 对象字面量方式
var obj1 = {
name: "Jack",
age: 26,
}
第二种: Object构造函数模式
var obj2 = new Object()
obj2.name = "Jack"
obj2.age = 26
第三种: 构造函数模式
function Test(name, age){
this.name = name
this.age = age
this.say = function(){
console.log('我能说话')
}
}
var obj3 = new Test('Jack', 26)
var obj4 = new Test('Rose', 25)
1.__proto__
__proto__
是每个对象在被创建时都有的一个属性,它指向该对象的原型(即构造函数的prototype
属性 XX.prototype)通过__proto__
,JavaScript 实现了原型链,当访问一个对象的属性时,如果该对象没有这个属性,就会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(null
)
let obj = {};
console.log(obj.__proto__ === Object.prototype); // true
2.prototype
prototype
是每一个对象(包括函数)所拥有的属性,它指向另一个对象,这个对象被称为原型对象,即xx.prototype,用来实例共享的属性和方法。每个函数在创建时都会自动获得一个prototype
属性。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
let alice = new Person('Alice');
alice.sayHello(); // 输出:Hello, my name is Alice
上面的例子中,Person.prototype原型对象包含了sayHello方法,所有由 Person
构造函数创建的实例都可以访问这个方法。
原型对象有一个constructor属性指向构造函数本身
Test.prototype.constructor === Test
// true
原型对象是一个普通的对象,它包含属性和方法。如上面的sayHello方法
原型对象的属性和方法会被继承到所有通过原型链与它相连的对象。
把方法写在原型对象上的好处是,所有实例在被创建出时便可使用次方法,而不用写到构造函数中占用内存
3.constructor
constructor(构造),
是每个对象都有的一个属性,它指向创建该对象的构造函数。通过constructor
属性,我们可以知道某个对象是由哪个构造函数创建的。
console.log(alice.constructor === Person); //true
原型链污染
定义:如果攻击者控制并修改了一个对象的原型,那将可以影响所有和这个对象来自同一个类、父类的对象,这种攻击方式就是原型链污染
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];
}
}
}
merge操作是最常见的可以控制键名的操作,也最能被原型链攻击。在合并过程中,复制操作的key如果为__proto__,便可能发生原型链污染
运行如下代码
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];
}
}
}
let o1 = {};
let o2 = {a:1,"__proto__":{b:2}};
merge(o1,o2);
console.log(o1.a,o1.b);//1 2
console.log(o1.__proto__ == o2.__proto__)//false
console.log(o1)//{ a: 1, b: 2 }
console.log(o2)//{ a: 1 }
o3 = {};
console.log(o1.__proto__ == o3.__proto__)//true
console.log(o2.__proto__)//{ b: 2 }
console.log(o3.b);//undefined
分析:当对o1,o2进行merge操作:merge(o1,o2)时,console.log(o1.a,o1.b)均有值1,2,这是否说明o1.__proto__已经被污染了呢?当我们创建o3对象时,console.log(o3.b)为undefined,说明并未发生原型链污染。o1.b之所以等于2,是因为在merge过程中,键__proto__虽然存在于o2中,但merge函数并没有对这个键进行特殊处理,它直接将source[key](即o2["__proto__"])的值{b:2}赋值给了o1,没有为o1添加一个新的__proto__键,所以最终得到了一个b属性。
运行如下代码
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];
}
}
}
let o1 = {};
let o2 = JSON.parse('{"a":1,"__proto__":{"b":2}}');
merge(o1,o2);
console.log(o1.a,o2.b);// 1 2
o3 = {};
console.log(o3.b);// 2
分析:在进行JSON解析的情况,merge函数进行合并的过程中,o2的__proto__将作为"键名"而不作为特殊属性被merge函数把o2的 __proto__
属性被合并到 o1
中。产生原型链污染,
但是此时o1__proto__不等于o2__proto__,因为它们只是内容相同,而属于不同的对象实例。o1__proto__ 等于o3__proto__