# Node.js原型链污染

# JavaScript原型链

JavaScript对象查找属性时,先看自身是否存在,如果自己没有就去让它“原型对象”里找,原型对象也没有就继续往上找直到null

每一个普通对象都有一个隐藏的引用,指向另一个对象,这个被指向的对象就叫它的原型对象,可以通过以下代码段查看{}的原型:

const user = {};
console.log(Object.getPrototypeOf(user));

普通对象{}的原型通常是Object.prototypeObject.prototype上有一些通用方法,例如:toStringhasOwnPropertyvaluerOf,所以对象可以调用这些方法

const user = {};
console.log(Object.getPrototypeOf(user)===Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));

所以普通对象的原型链大概是:

user->Object.prototype->null

# __proto__

__proto__Object.prototype上的一个访问器属性,用来暴露一个对象内部的[[Prototype]],他可以读取对象的原型也可以尝试修改对象的原型。普通对象之所以能访问obj.__proto__,是因为他从Object.prototype继承了访问器属性

const user = {};
console.log(user.hasOwnProperty("__proto__"));

但是:

const user = {};
console.log(Object.prototype.hasOwnProperty("__proto__"));

__proto__Object.prototype上由getter和setter组成的访问器属性,并且他不总是可靠存在,null原型对象不会继承它

并且Object.getPrototypeOf() === obj.__proto__

prototype:函数才有的属性,用来给它创建出来的实例当原型:

function ZLARYY(){}
const Zl47yY=new ZLARYY();
console.log(Zl47yY.__proto__===ZLARYY.prototype);

Zl47yY.__proto__指向ZLARYY.prototypeZLARYY.prototype是ZLARYY函数的原型

# 原型链污染

原型链污染指的是通过对象的某些特殊属性可以将属性写到公共原型对象上,导致很多普通对象被污染。

现有一段代码:

const ZLARYY = {};
console.log(ZLARYY.isAdmin);
Object.prototype.isAdmin = true;
console.log(ZLARYY.isAdmin);

原本ZLARYY并不具有isAdmin属性,但是在给Object.prototype添加上isAdmin属性之后可以发现ZLARYY也具备访问isAdmin属性的能力,而ZLARYY只是一个普通对象

# merge()

之前我们提到过python原型链污染里面的merge函数,在nodejs原型链污染中也具有这样的merge函数:

function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === "object") {
      if (!target[key]) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

在该段代码中,如果source的是一个对象并且target[key]为空,那么就执行target[key]={}之后再进行一次merge(target[key],source[key],否则直接进行赋值target[key]=source[key]

其存在原型链污染的原理是递归合并对象的逻辑,正常情况下是希望把一个对象里的属性合并到另一个属性里

function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === "object") {
      if (!target[key]) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}
const ZLARYY={
    age:18
};
const source={
    sex:"man"
};
merge(ZLARYY,source);
console.log(ZLARYY);

同样,该函数也能对已有属性进行修改:

function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === "object") {
      if (!target[key]) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}
const ZLARYY={
    age:18
};
const source={
    age:19
};
merge(ZLARYY,source);
console.log(ZLARYY);

不过我们的merge逻辑是深度合并,所以我们能发现可以实现对象内部对象的合并:

function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === "object") {
      if (!target[key]) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}
const ZLARYY={
    age:18,
    friends:{
        Arache:28,
        Chesmond:29
    }
};
const source={
    sex:"man",
    friends:{
        SusU:"best"
    }
};
merge(ZLARYY,source);
console.log(ZLARYY);

可以发现最后得到的结果不是

{
  age: 18,
  friends: { SusU: 'best' },
  sex: 'man'
}

如果我们的source是恶意数据:

'{"__proto__":{"isAdmin":true}}'

这和我们的python原型链污染是一样的

# setvalue()

很多后端存在路径赋值的场景,比如setvalue()函数,其正常使用就是将path里面的属性给修改为参数中的value:

function setValue(obj, path, value) {
  const keys = path.split(".");
  let current = obj;

  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];

    if (!current[key]) {
      current[key] = {};
    }

    current = current[key];
  }

  current[keys[keys.length - 1]] = value;
}

当我们执行setValue(obj,"__proto__.isAdmin",true),也能实现和merge函数一样的功能

# constructor.prototype污染

除了__proto__还有一种常见方式是constructor.prototype,因为普通对象的constructor.prototype通常指向Object

const ZLARYY={
    age:18,
    friends:{
        Arache:28,
        Chesmond:29,
        SusU:"best"
    }
};
ZLARYY.constructor.prototype.isAdmin=true;
console.log(ZLARYY.isAdmin);

其实ZLARYY.constructor.prototype.isAdmin=true;就等价于Object.prototype.isAdmin=true;