如果你想弄明白什麼怎樣才可以實現JavaScript的原型鏈汙染,那麼你首先需要弄清楚兩個東西,那就是__proto__
和prototype
。
到底什麼才是__proto__
和prototype
?
那我們先來看看比較官方的說法吧:
__proto__:是每個物件的隱藏屬性,指向建立該物件的建構函式的原型物件(prototype)。它是物件用於繼承屬性和方法的機制。它是一個物件。所有的物件都可以透過__proto__ 來訪問它的原型,進而實現原型鏈查詢。
prototype是函式(特別是建構函式)特有的屬性,它用於定義由該建構函式建立的例項共享的屬性和方法。prototype通常用來定義建構函式的例項方法。當我們建立一個新物件時,該物件會繼承其建構函式 prototype上的屬性和方法。
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
在這個例子中,obj
是透過Object
建構函式建立的,所以obj.__proto__
指向Object.prototype
。
那我們可以這樣去理解,prototype是類通有的屬性,當類被例項化為物件後,物件會擁有prototype中的屬性和方法。當物件想去訪問類的原型時用__proto__
屬性來訪問類的原型。
prototype鏈
瞭解了__proto__
和prototype
之後,那我們就要去深入瞭解JavaScript的繼承鏈,在JavaScript中,每個物件都有一個指向其原型的內部連結(即__proto__
),這個原型本身也是一個物件,通常還有自己的原型,這樣就形成了一條原型鏈。當你訪問一個物件的屬性時,JavaScript 會沿著這條原型鏈逐級查詢,直到找到該屬性或者原型鏈的頂端(即Object.prototype)。
function Animal(name) {
this.name = name;
}
function Something() {
this.speak = console.log(this.name + ' makes a beautiful sound.');
};
const dog = new Animal('Dog');
dog.speak; // 輸出: 'Dog makes a beautiful sound.'
例項化Animal類建立了dog物件,當訪問speak屬性時,在dog中尋找不到時,會在dog.__proto__.__proto__
中尋找,發現dog.__proto__.__proto__
中有speak屬性,也就是Something.prototype
中存在speak屬性。也就是說JavaScript使用prototype鏈實現繼承機制。
prototype鏈汙染(原型鏈汙染)
那我們學習繼承、prototype鏈不就是為了進行鏈子汙染,然後得到我們想得到的東西嗎?
既然dog.__proto__.__proto__
指向建立該物件的建構函式的原型物件(Something.prototype
)。那麼我們修改dog.__proto__.__proto__
的內容是不是可以進而實現了修改Something類。
示例:
function Animal(name) {
this.name = name;
}
function Something() {
this.speak = console.log(this.name + ' makes a beautiful sound.');
};
const dog = new Animal('Dog');
dog.__proto__.speak = console.log('修改成功');
dog.speak;
透過這個結果我們可以看出我們輕易的將dog.__proto__.__proto__
的speak值進行了更改。我們先將這個繼承鏈給搞出來:
dog -> Animal.prototype -> Something.prototype -> Object.prototype -> null
那麼攻擊者如果透過一個入口點,控制並修改了一個物件的原型,那麼將可能影響所有和這個物件來自同一個類、父類直到Object類的物件。
典型的便是merge操作導致原型鏈汙染:
javascript
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 object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)
object3 = {}
console.log(object3.b)
//1 2
//2
在JSON解析的情況下,__proto__
會被認為是一個真正的“鍵名”,而不代表“原型”,所以在遍歷object2的時候會存在這個鍵。