JS原型链污染学习笔记

JS基础还是太烂…先记录一下基础知识

0x00 基础知识

这一部分是跟着阮一峰写的教程来记录的,与原型链污染没多大关系

ECMAScript 6

ECMAScript 6,简称为ES6,是JavaScript语言的下一代标准,已在2015年发布;ES6与JS的关系是,前者是后者的规格,后者是前者的一种实现

let、var和const

1.var是声明全局变量的命令

2.ES6新增了let命令,它和var的区别在于:let声明的变量只在let命令所在的代码块有效

3.const声明一个只读的常量,一旦声明便不能再更改;作用域和let命令相同

例如,使用var

1
2
3
4
{
var i = 'gtfly';
}
console.log(i); // gtfly

使用let

1
2
3
4
{
var i = 'gtfly';
}
console.log(i); // ReferenceError: i is not defined

块级作用域

ES5只有全局作用域和函数作用域,ES6的let实际上为JS新增了块级作用域

例如:

1
2
3
4
5
6
7
function test() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}

上面定义了两个块级作用域,并且都声明了变量n,而且内层作用域并不影响外层作用域

ES6允许块级作用域的任意嵌套:

1
2
3
4
5
6
7
{
{
{
let a = 'gtfly';
}
}
}

字符串

1.ES6加强了对unicode字符的支持,可直接转换在\u0000~\uFFFF之间的字符;如果超过这个范围,可将码点放入大括号就能正确转换:

1
2
3
4
5
> console.log('\u0061');
a

> console.log('\u{20BB7}');
𠮷

2.字符串可以被for...of循环遍历

1
2
3
4
5
6
7
8
> for(let i of 'gtfly'){
... console.log(i);
... }
g
t
f
l
y

3.ES6引入了模板字符串,用反引号标识

1
2
3
> let name = 'gtfly';
> console.log(`Hello ${name}`);
Hello gtfly

模板字符串可以紧跟在一个函数名后面,该函数被调用来处理这个模板字符串,这被称为标签模板功能,其中,“标签”指的是函数,“模板”就是它的参数

1
2
3
alert`hello`
// 等同于
alert(['hello'])

对象

1.ES6允许在大括号里面直接写入变量和函数,作为对象的属性和方法

例如:

var person = {name:'gtfly'}

另一个例子:

1
2
3
4
5
6
7
8
9
10
11
function f(x, y) {
return {x, y};
}

// 等同于

function f(x, y) {
return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

2.方法的简写:

1
2
3
4
5
6
7
8
9
10
11
12
13
const o = {
method() {
return "Hello!";
}
};

// 等同于

const o = {
method: function() {
return "Hello!";
}
};

3.基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已

生成实例对象的传统方法是使用构造函数:

1
2
3
4
5
6
7
8
9
10
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

4.类的所有实例共享一个原型对象

1
2
3
4
5
6
7
function Point(name){
this.name = name;
}

p1 = new Point('test1');
p2 = new Point('test2');
p1.__proto__ === p2.__proto__ // true

上述p1和p2都是Point的实例,它们的__proto__都是Point.prototype

变量的解构赋值

1.数组

普通赋值:

1
2
3
let a = 1;
let b = 2;
let c = 3;

ES6允许如下方式解构赋值:

1
let [a, b, c] = [1, 2, 3];

指定默认值:

1
let [x, y='aaa'] = ['b']

2.字符串

1
2
3
4
5
6
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

0x01 prototype与_proto_

先说一下结论:

prototype是一个函数(类)的属性,所有类对象在实例化的时候会拥有prototype中的属性和方法;它的作用是让该函数所实例化的对象们都可以找到公用的属性和方法

__proto__是一个对象的属性,指向这个对象所在的类的prototype属性;它的作用是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象里找

例如:

0x02 原型链继承

下面定义了两个构造函数Father()Son(),其中Son()prototype

在调用son.last_name时,JS引擎会做如下操作:

1.在对象son中寻找last_name

2.如果找不到,则在son.__proto__中寻找last_name

3.如果仍然找不到,继续在son.__proto__.__proto__中寻找last_name

4.直到找到null结束,即null作为原型的终点

0x03 原型链污染

上面定义了一个test1对象,并用__proto__去修改它的类的name属性,可以看到,修改后test1的name属性值并没有变化,因为上面说过在寻找属性时,会先在对象中寻找;

但我们接着又定义了一个test2空对象,可以看到它的name属性值是我们通过__proto__修改的值,这是为什么呢?

JS直接创建对象实例,有以下方式:

方法1:

1
2
let person = new Object();
person.name = 'gtfly';

方法2:

1
let person = {name:'gtfly'};

也就是说我们直接定义的对象,他们都是从Object类实例化的;因此上面我们通过修改对象的__proto__的属性,实际上修改的是Object类的属性,那么修改后便会影响所有实例化/继承Object类的对象。这种攻击方式就是原型链污染

0x04 原型链污染场景

1.对象合并(merge)

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

虽然o2这样定义,但它并不存在__proto__这个key,因为它会把__proto__当做原型;因此此时o2只有两个key:ab

更改如下:

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

JSON解析的情况下,__proto__会被认为是一个真正的键名,因此o1和o2进行merge时,会进行如下操作:

1
o1['__proto__'] = {"b":2}

然后创建一个新的空对象,会发现它会拥有b属性:

1
2
let o3 = {};
o3.b; // 2

参考文章

https://blog.csdn.net/cc18868876837/article/details/81211729#commentBox

https://www.freebuf.com/articles/web/200406.html

https://meizjm3i.github.io/2018/09/11/JavaScript_Prototype_Pollution/