avaScript 是一種基于原型的面向對象語言。雖然你經常會看到class關鍵字,但它的底層本質還是用作原型。
在本文中,我們將了解 JavaScript 的原型性質,以及對象中的原型鏈。
首先檢查以下代碼:
const animals = {
name: "animal",
type: "object",
}
animals.hasOwnProperty("name")// true
但是我們并沒有給animals定義方法hasOwnProperty,可它為什么可以調用該方法?在本文結束時,我們將了解其工作原理以及更多內容。
一、原型的概念
根據查閱得到的字面意思的解釋是:
指一個詞語或一個類型意義的所有典型模型或原形象,是一個類型的典型特征,我們可以用下圖中的車輛示例很好地解釋這一點。
原型“A”是創建其他版本(如“B”和“C”)的第一個版本。“A”包含車輛應具備的最基本功能,而“B”和“C”將包含更多的功能。
這意味著,“B”和“C”是“A”的改進版本,但仍包含“A”的特征。“A”有四個輪胎,“B”也有四個輪胎,但是可以飛,“C”同樣有四個輪胎,但是可以在水上行駛。
JavaScript 在原型的基礎上工作。在每個函數的聲明中,JavaScript 引擎將prototype屬性添加到該函數,這使該函數成為可以創建其他版本的原型。我們可以通過打印其屬性確認:
function hello() {
console.log("hello")
}
console.dir(hello)
結果:
如上圖所示,顯示了函數的屬性,hello函數中包括了prototype屬性, 以及另一個名為__proto__的屬性。本文稍后會詳細介紹。
該prototype對象有兩個屬性:一個名為constructor以及另一個同樣名為__proto__的屬性。前者指向hello函數,后者指向Object。
二、原型的好處
說原型的好處之前我們先說一下構造函數,這是創建對象的一種方式,如下所示:
function Hello() {
console.log("hello")
}
const anotherVersion = new Hello()
anotherVersion.type = "new"
console.log(anotherVersion)
Hello首字母大寫是一種約定,表示該函數可以用作構造對象,這個函數也被稱為構造函數。
結果:
結果現在向我們展示了這個anotherVersion對象是一個從Hello函數通過new關鍵字而變化來的。你可以通過這種方式去創建類似具有相同功能的對象,例如:
function Obj(name) {
this.name = name;
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
const java = new Obj("java")
console.log(javascript)// Obj {name: 'javascript', printName: f}
console.log(java)// Obj {name: 'java', printName: f}
構造函數中的this變量指向構造函數new出來的實例化對象(在上面的代碼中是javascript和java)。
我們可以看到,雖然javascript和java具有不同的名稱值,但它們具有相同的功能代碼。
使用原型的好處就是,你可以通過一個構造函數去創建很多具有相同功能的對象,并且這些對象都具有不同的名字。
還記得上面hello函數有兩個屬性:prototype和__proto__。prototype還有兩個屬性:constructor和__proto__。使用構造函數創建對象時,使用了prototype屬性的constructor屬性,讓我們用下面的代碼檢查一下:
function Obj(name) {
this.name = name
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
console.log(javascript)
結果:
從上圖中,你會看到__proto__屬性連接到我們的構造函數Obj。
三、與原型共享功能
現在我們知道函數的prototype屬性使該函數成為可用于創建其他對象的原型。
如果該prototype屬性有其他屬性呢?我們知道,JavaScript 對象可以在任何時候添加新的屬性,讓我們來看看:
function Obj(name) {
this.name = name
this.printName = function () {
console.log(this.name)
}
}
const javascript = new Obj("javascript")
Obj.prototype.printType = function () {
console.log(this.type)
}
console.log(javascript)
結果:
如上圖所示,__proto__屬性現在有一個printType方法,但對象javascript本身沒有printType方法。由上面所述結果我們可以知道,__proto__屬性連接我們的構造函數,由于javascript在默認情況下可以訪問__proto__屬性中的構造函數,因此它也可以訪問printType。因此,以下操作將正常工作:
javascript.printType()// undefined
javascript.type = "language"
javascript.printType()// language
JavaScript 是如何做到這一點的呢?首先它檢查對象是否存在該方法,如果不存在,它檢查__proto__屬性。
四、原型鏈
我們看最后一張圖片,你會注意到車輛B和C也有自己的原型,這意味著Obj用作原型的對象也繼承了另一個原型的一些特性,這稱為原型鏈。
這說明,一個對象可以是原型的新版本,同時也是另一個對象的原型。因此,當你嘗試訪問對象上的屬性時,JavaScript引擎開始從對象自身中查找該屬性,如果沒有,它會繼續檢查__proto__,一直到沒有__proto__或者找到該屬性。如果找到最后,此屬性不存在時,返回undefined。
五、總 結
回到第一個代碼塊:
const animals = {
name: "animal",
type: "object",
}
animals.hasOwnProperty("name")// true
到現在你應該清楚了,對吧?
當你的animals在控制臺打印的時候,您會注意到它有一個__proto__指向Object的原型。并且,Object的原型具有hasOwnProperty屬性。animals繼承了該屬性,這使得它可以使用該屬性。
Object在 JavaScript 中有一個所有對象都能繼承的原型。Function、String等構造函數也從Object繼承了屬性。
這也就是為什么string.toLowerCase()也可以直接使用的原因。構造函數的原型對象String具有所有這些屬性,因此字符串可以使用它們。
在瀏覽某個論壇的時候,第一次看到了JavaScript原型鏈污染漏洞。當時非常的好奇,當時我一直以為js作為一種前端語言,就算存在漏洞也是針對前端,不會危害到后端,因此我以為這種漏洞危害應該不大。可當我看到他的漏洞危害還有可以執行任意命令的時候,發現可能我想到有點簡單了。js也是可以用來做后端語言的。這篇文章就來認識一下這個漏洞。
既然漏洞名稱是JavaScript原型鏈污染,那么首先就要先明白JavaScript原型鏈是什么。
正如我們所知,Javascrip的復雜類型都是對象類型(Object),而js不是一門完全面對對象編程的語言。那么對于對象編程來說要考慮對象的繼承。
js實現繼承的核心就是原型鏈。我理解的就是原型鏈的存在就是js中的繼承機制,保證函數或對象中的方法,屬性可以向下傳遞。
js使用了構造函數來創建對象,如下,我們可以通過構造函數來定義一個類:
// 構造函數
function Person(name, age) {
this.name = name;
this.age = age;
}
// 生成實例
const p = new Person('zhangsan', 18);
可以看到這個類除了我們定義的兩個屬性以外,還有一個prototype的屬性。prototype指向函數的原型對象,這是一個顯式原型屬性,只有函數才擁有該屬性。
prototype也擁有兩個屬性:
constructor:指向原型的構造函數
prototype:指向了Object的原型
在prototype的屬性中有一個_proto_,那么這個_proto_與prototype又有什么關系?
原型prototype是類的一個屬性,而所有用類實例化的對象,都將擁有這個屬性中的所有內容,包括變量和方法。比如上圖中的p對象,其天生就具有類Person的屬性和方法。
我們可以通過Person.prototype來訪問Person類的原型,但Person實例化出來的對象,是不能通過prototype訪問原型的。這時候,就該__proto__登場了。
也就是類可以用prototype來訪問類的原型,而實例化的對象可以用_proto_來訪問對象所在類的prototype屬性。
總結:
其實我們只要明白一點就可以了,JavaScript原型鏈是js中實現繼承的核心,js的對象都會執行其它的原型,最后指向的原型為null。最后關于原型鏈再來總結一下
1)js是通過原型鏈來實現繼承的。
2)所有類對象在實例化的時候將會擁有prototype中的屬性和方法
3)類可以使用prototype來訪問類的原型對象,而實例化對象可以通過_proto_來訪問類的原型對象
let f = new Foo();
f.constructor === Foo;
f._proto_ === Foo.prototype
f._proto_ === Foo.prototype
Foo._proto_ === Function.prototype
在了解了原型鏈的相關知識以后,可以來看看竟然什么是原型鏈污染漏洞。
上面說過實例化對象的__proto__指向了類的prototype。那么,如果我們修改了實例化對象__proto__中的值,是不是就可以修改類中的值呢?是否可以影響所有和這個對象來自同一個類、父祖類的對象?
其實這就是原型鏈污染的原理。我們通過修改實例化對象的__proto__中的值,污染了類本體,進而影響所有和這個對象來自同一個類、父祖類的對象。
p神的博客上有一個這樣的例子,用來說明原型鏈污染:
在實際的情況中,我們可能只能控制部分參數,那么我們怎么才能為__proto__賦值呢?
要為__proto__賦值就要求__proto__作為變量傳進去并且作為鍵名,這種情況一般出現在下面的三種場景中:
下面借用p神文章中的一個例子來看看具體操作,原文鏈接為:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
以對象merge為例,我們想象一個簡單的merge函數:
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]
}
}
}
在合并的過程中,存在賦值的操作target[key] = source[key],那么,這個key如果是__proto__,是不是就可以原型鏈污染呢?
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
結果是,合并雖然成功了,但原型鏈沒有被污染:
這是因為,我們用JavaScript創建o2的過程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已經代表o2的原型了,此時遍歷o2的所有鍵名,你拿到的是[a, b],__proto__并不是一個key,自然也不會修改Object的原型。從下面的圖中也可以看出,在o1中,參數b并沒有出現在原型中。
那么,如何讓__proto__被認為是一個鍵名呢?
我們將代碼改成如下:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
可見,新建的o3對象,也存在b屬性,說明Object已經被污染
這是因為,JSON解析的情況下,__proto__會被認為是一個真正的“鍵名”,而不代表“原型”,所以在遍歷o2的時候會存在這個鍵。
再來看看o1發現,b屬性是定義在原型之中的。
merge操作是最常見可能控制鍵名的操作,也最能被原型鏈攻擊,很多常見的庫都存在這個問題。
接下來用一個cve漏洞來具體再看一下這個漏洞。
3.4.0版本之前的jQuery存在一個原型污染漏洞CVE-2019-11358,PoC如下。
//代碼如下,如果從前端接收一個json內容,傳到后端。
//json內容:JSON.parse('{"__proto__": {"z": 123}}')
const json1 = ajax();
jQuery.extend(true, {}, JSON.parse(json1));
console.log( "test" in {} ); // true
jQuery.extend () 函數用于將一個或多個對象的內容合并到目標對象
$.extend( [deep ], target, object1 [, objectN ] )
參數 | 描述 |
deep | 可選。 Boolean類型 指示是否深度合并對象,默認為false。如果該值為true,且多個對象的某個同名屬性也都是對象,則該"屬性對象"的屬性也將進行合并。 |
target | Object類型 目標對象,其他對象的成員屬性將被附加到該對象上。 |
object1 | 可選。 Object類型 第一個被合并的對象。 |
objectN | 可選。 Object類型 第N個被合并的對象。 |
再來看看實際的代碼是如何去寫入的。
首先下載jQuery,這里下載的是3.3.0版本
https://github.com/jquery/jquery/tree/3.3.0
在src/core.js中文件中可以找到該extend函數。看過源碼,可以發現該函數的正好符合上面說的合并數據的概念,那么來看看它到底會不會被污染?
我們來動態調試一下這個程序:
引入jQuery腳本,并設置斷點,進行調試
首先根據第一個參數判斷是否進行深度拷貝,然后進行第一次循環,取得參數為__proto__
第二次循環,在__proto__中去參數進行賦值
此時再看原型已經被污染了
js原型鏈污染可以說原理并不是太難懂,關鍵是實際中如何去利用。關于這個漏洞也是看了很多大神的文章,它們的思路真的太厲害了,我還有很多需要學習的,跟大家一起共勉。
由于本人水平有限,文章中可能會出現一些錯誤,歡迎各位大佬指正,感激不盡。如果有什么好的想法也歡迎交流,謝謝大家了~~
https://www.freebuf.com/articles/web/275619.html
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://xz.aliyun.com/t/7025
https://www.freebuf.com/articles/web/264966.html
作者:Notadmin
文章來源:FreeBuf
avascript的原型鏈圖
各種對象的__proto__和函數的prototype 都闡明了
不能保證100% 正確(有問題直接注釋或者私信偶) 但是互聯網上原型鏈圖比這個全的偶沒見過(右上角的Number Date們需要改進)
可以改進但是 不能再簡單了
有點暈吧 大部分人都會暈
所有網上 一堆 xxx.__proto__.__proto__.__proto__ 皆可由此圖搞定
那些刁鉆古怪的問題退化為孔乙己的茴字問題
死記硬背吧 先 要不改行
90%的前端或者js程序員或者老師們對Javascript懂得不比這個多 嘿嘿
給手機看的
但是這個圖里的所有褐色單向箭頭鏈就是Javascript的原型鏈(顏色標注對理解js原型鏈很關鍵)
原型鏈大部分時候是不可見的(__proto__在firefox nodejs中可見)那么圖退化為
上二圖三特點
1.所有對象都在原型鏈上
2.除了null 每個對象都有且唯一的__proto__原型對象
3.除了null, Object.prototype,其它對象的原型對象雖然不可通過.__proto__操作訪問 但是通過原型鏈上某個構造器(函數)的prototype屬性都可以訪問到
js coder大多時候要面對的是
優雅了吧 :-()
所有javascript重度編碼都是操作上面這個圖的元素 但是你心里至少要有下圖
如果連這圖都嫌棄不好記 最簡單的鐵三角 javascript 99%的幻化都由此來
原型鏈的本質是嘛?以后分解 嘿嘿
*請認真填寫需求信息,我們會在24小時內與您取得聯系。