文章目录
- 前置知识
- JavaScript数据类型
- prototype原型
- 同步和异步
- child_process模块
- 原型链污染
- 利用条件
前置知识
JavaScript数据类型
let和var关键字的区别
使用var或let关键字可以定义变量
let和var的区别如下:
- var是全局作用域,let 只在当前代码块内有效
- 当在代码块外访问let声明的变量时会报错
- var有变量提升,let没有变量提升
- let必须先声明再使用,否则报Uncaught ReferenceError xxx is not defined;var可以在声明前访问,只是会报undefined
- let变量不能重复声明,var变量可以重复声明
普通变量
var a=1;
let b=2;
数组变量
var a = new Array();
var b= [];
字典
var a = {};
var b= {"foo":"bar"};
prototype原型
JavaScript只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为
__proto__
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上知道一个对象的原型对象为null。
实例图
我们编写代码测试下
源码
function Son(){};
var son = new Son();
console.log(Son.prototype)
console.log(son.__proto__)
console.log(Son.prototype == son.__proto__)
不难发现Son.prototype == son.__proto__
是成立的
然后再了解下继承
原型是继承的基础,JavaScript中原型链可以通过prototype这个属性来实现继承机制
实例代码
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类,在调用son.last_name
时,首先在son对象里找,如果没有找到,就会在son.__proto__
(也就是Son.prototype),找不到就再往上son.__proto__.__proto__
里找 ,知道null为止
同步和异步
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的
fs.readFileSync()。异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
很简单理解,当你先读取文件输出后输出一段话的时候
同步:先输出文件内容,再输出一段话
异步:先输出一段话,后输出文件内容
就比如,涉及同步和异步的问题我们使用的exec是异步进程,在我们输入ls,查取目录时,就已经eval执行了,所以我们要使用创造同步进程的函数如execSync
child_process模块
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出。
其中的一些函数,在一些情况下,可以导致命令执行漏洞
原型链污染
下面用代码解释下
let foo = {bar: 1}
console.log(foo.bar)
foo.__proto__.bar = 2
console.log(foo.bar)
let zoo = {}
console.log(zoo.bar)
可以发现当我们修改foo的原型时,由于查找顺序的原因,foo.bar仍为1;如果我们重新创建空对象,发现成功进行原型链污染
利用条件
原型链污染在哪些情况下可以实现呢,其实找找能够控制数组(对象)的“键名”的操作即可
- 对象merge
- 对象clone(其实内核就是将待操作的对象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的原型。
绕过方法很简单,修改代码如下
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)
这是因为,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。