JavaScript原型鏈汙染探討

weljoni發表於2024-10-12

如果你想弄明白什麼怎樣才可以實現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;

image

透過這個結果我們可以看出我們輕易的將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的時候會存在這個鍵。

相關文章