名字带java的我都不会,开学!

继承与原型链

继承

JavaScript中搜索属性和方法会从这个对象本身开始,按照原型链继续搜索该对象的原型,也就是obj.[[Prototype]](或者说是obj.__proto__,但不建议直接取用__proto__属性),以及该对象的原型的原型,直到找到一个名字匹配的属性或者到达原型链的末尾(null)。

继承的可以是属性,如:

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
  },
};

// o.[[Prototype]] 具有属性 b 和 c。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype(我们会在下文解释其含义)。
// 最后,o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。
// 这是原型链的末尾,值为 null,
// 根据定义,其没有 [[Prototype]]。
// 因此,完整的原型链看起来像这样:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null

console.log(o.a); // 1
// o 上有自有属性“a”吗?有,且其值为 1。

console.log(o.b); // 2
// o 上有自有属性“b”吗?有,且其值为 2。
// 原型也有“b”属性,但其没有被访问。
// 这被称为属性遮蔽(Property Shadowing)

console.log(o.c); // 4
// o 上有自有属性“c”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“c”吗?有,其值为 4。

console.log(o.d); // undefined
// o 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype 且
// 其默认没有“d”属性,检查其原型。
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null,停止搜索,
// 未找到该属性,返回 undefined。

当然,原型链可以更长,如:

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
    __proto__: {
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

console.log(o.d); // 5

也可以是方法,如:

const o = {
  a: 1,
  b: 2,
  // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
  __proto__: {
    b: 3,
    c: 4,
    sum: function() {
      return this.a + this.b + this.c;
    },
  },
};

console.log(o.sum()); // 8

构造函数

在JavaScript中,如果我们需要构造一个类,我们需要定义一个构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = new Person('Tom', 18);

这段代码中,Person函数的内容就是Person类的构造函数,而nameage就是Person类的属性。

当然,为了简化代码,我们可以使用ES6的class语法糖来定义类,但是本质上还是构造函数。

一个类中总会有一些方法,和属性类似,我们可以直接写在构造函数中:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
  }
}

const person = new Person('Tom', 18);
person.sayHello(); // Hello, my name is Tom, I'm 18 years old.

但是这样写的问题在于,每次我们创建一个新的实例时,都会创建一个新的sayHello方法,这样会造成内存的浪费,所以我们可以将sayHello方法写在Person类的原型上:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
}

const person = new Person('Tom', 18);
person.sayHello(); // Hello, my name is Tom, I'm 18 years old.

此时person.__proto__就是Person.prototype

图 0

与上文说的继承机制一样,如以下代码:

function Father(){
    this.firstName = 'Tom';
    this.lastName = 'Smith';
}

Father.prototype.getFullName = function(){
    return this.firstName + ' ' + this.lastName;
}

function Son(){
    this.firstName = 'Jerry';
}

Son.prototype = new Father();

const son = new Son();

console.log(son.getFullName()); // Jerry Smith

在这段代码中,获取sonfirstName时,先匹配到了Son类的firstName,也就是Jerry,而获取sonlastName时,由于Son类中没有lastName属性,所以会继续向上搜索,匹配到了Father类的lastName,也就是Smith

原型链污染

什么是原型链污染

前文说过,obj.__proto__ == Obj.prototype,所以我们可以通过修改obj.__proto__来修改Obj.prototype,从而污染原型链。而且所有的原型链都能回溯到Object.prototype,所以我们可以通过污染Object.prototype来污染所有的原型链。

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar) // 1

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar) // 1

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar) // 2

这个神奇的现象是由于foo.__proto__Object.prototypezoo.bar回溯到了Object.prototype,所以即使zoo是一个空的对象,zoo.bar也是2

实际应用

一般在普通JS中,即使有原型链污染也无法造成服务端的污染。所以一般原型链污染的对象是Node.js。

出了道简单的原型链污染题目:

const express = require('express')
const session = require('express-session')

let app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
    key: 'SESSION_ID',
    secret: 'xxxxxx',
    resave: false,
    saveUninitialized: true,
    cookie: {
        maxAge: 60 * 1000 * 30
    }
}))

let users = {
    'admin': {
        'username': 'admin',
        'age': 18,
        'sex': 'male',
        'isAdmin': true,
    },
    'guest': {
        'username': 'guest',
        'age': 25,
        'sex': 'female',
    }
}

app.get('/', (req, res) => {
    req.session.username = 'guest'
    console.log(req.session.username)
    res.send(req.session.username + '<br> Age: ' + users[req.session.username]['age'] + '<br>Sex: ' + users[req.session.username]['sex'])
})


app.get('/get_flag', (req, res) => {
    console.log(req.session.username)
    if (users[req.session.username]['isAdmin'] !== true) {
        res.send('You are not admin!')
        return
    }

    res.send('CNSS{W0W_Y0U_P01luT3d_7h3_Pr0t0Typ3_Ch41n!}')
})

app.post('/update_userinfo', (req, res) => {
    let newUserInfo = req.body
    console.log(newUserInfo)
    console.log(req.session.username)
    if (req.session.username !== newUserInfo.username) {
        res.send('You can only update your own info!')
        return
    }

    if (newUserInfo.key == 'isAdmin') {
        res.send('Hacker!')
        return
    }

    
    users[newUserInfo.username][newUserInfo.key] = newUserInfo.value;

    res.send('Update success!')
})

let server = app.listen(8081, () => {
    let host = server.address().address
    let port = server.address().port
    console.log("应用实例,访问地址为 http://%s:%s", host, port)
})

这个场景的预期传参如下:

{
    "username": "guest",
    "key": "age",
    "value": 20
}

并且由于判断限制,我们不能修改isAdmin属性。

但是,由于guest并没有isAdmin属性,只需要修改他的原型有isAdmin属性即可。如果我们将key设置为__proto__,value设置为{isAdmin: true},那么服务端将会执行users['guest']['__proto__'] = {isAdmin: true},从而使guest.isAdmin (=> guest.__proto__.isAdmin) == true

{
    "username": "guest",
    "key": "__proto__",
    "value": {
        "isAdmin": true
    }
}

图 1

图 2

References

继承与原型链 - JavaScript | MDN
深入理解 JavaScript Prototype 污染攻击 | 离别歌