名字带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
类的构造函数,而name
和age
就是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
。
与上文说的继承机制一样,如以下代码:
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
在这段代码中,获取son
的firstName
时,先匹配到了Son
类的firstName
,也就是Jerry
,而获取son
的lastName
时,由于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.prototype
,zoo.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
}
}
References
继承与原型链 - JavaScript | MDN
深入理解 JavaScript Prototype 污染攻击 | 离别歌