KeenLab Tech Talk(二)| 淺談React框架的XSS及後利用
主講人簡介
ashx:
騰訊安全科恩實驗室安全研究員、Katzebin戰隊副隊長、TCTF出題人之一;主要研究Web安全,在各類的Web應用中發掘不少高危漏洞;作為A*0*E與Katzebin成員參與多場CTF競賽。
前言
隨著前端技術的高速發展,前後端分離的開發模式已經深入人心。由於後端不再直接輸出頁面,轉以API介面的方式提供服務,導致XSS在這類新專案中不再常見。 不過安全永遠是一個動態的過程,新的開發技術會帶來新的攻擊面。 現今前端最常用的主要是三個框架:React、Vuejs和Angular,本文將具體介紹基於React框架開發的前端應用的攻擊面。 其他框架也可依此類推。
React應用長什麼樣?
可以使用 Create React App ( https://create-react-app.dev/ ) 來快速生成並執行一個React應用。
安裝了 React Developer Tools
( https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi ) 並訪問一個網站後,若瀏覽器 DevTool會亮起“Component”等標籤,則代表這個網站使用了React框架開發。
安全從業者往往會需要對一個網站的JavaScript程式碼進行審計。 由於幾乎所有的React開發的專案都使用了JSX,因此React框架通常會配合Webpack(parcel / rollup)、babel(tsc)等前端編譯流程工具使用, 如果沒有Sourcemap就很難從被編譯的原始碼還原出原始React程式碼。
什麼是JSX?
JSX是一種JavaScript的語法擴充套件,通常和React配合使用。Vuejs也支援JSX。JSX允許開發者直接在JavaScript內部編寫XML語法,不需要經過各種字串的中轉。由於沒有瀏覽器支援JSX, 導致React應用的的開發環境通常需要一個編譯器負責將JSX編譯為瀏覽器可以識別的JS程式碼。
編譯器(通常是babel)會將以下JSX程式碼:
const element = ( <h1 className="greeting"> Hello, world! </h1>);
編譯成以下能在瀏覽器中執行的JavaScript程式碼(結構有做簡化)
const element = { type: 'h1', props: { className: 'greeting',
children: 'Hello, world!' }};
沒有什麼DOM結構了,有的只有一個個Object。
我們注意這裡的
Hello, world! 對應的 children ,如果我們可以控制這個屬性,是否可以進而導致XSS?答案是否定的。當 children 的型別是字串時,React將對DOM元素使用innerText來設定children,因此不會出現XSS;若可以將其控制為Object(這通常很難),但由於它的隱藏屬性 $$typeof 的值,它 在高版本的React中的型別是 Symbol ( Symbol 是ES6中引入的一種能表示一種唯一值的型別, Symbol('123') == Symbol('123') 的結果是 false ), 我們也無法讓React渲染這個物件。
所以對於一個基於React框架(Vue和Angular同樣)編寫的前端專案,即使沒有對XSS字串進行特殊過濾,一般也可以認為是安全的。但世界上總有喜歡劍走偏鋒的開發者,在某些React開發的專案內,仍然可以挖掘到不少XSS。
常規XSS
一些開發者未系統地學習React框架的思想,導致他們可能會使用各類DOM API來繞過React對DOM的管理。這些API包括 document.write 、 document.appendChild 等。 可以直接全文搜尋這些API。
以下列舉的程式碼均來源於GitHub公開搜尋。
如下圖, 該專案儘管使用了React,但同時還在使用DOM API。此處 innerHTML 如果可控(該專案中不可控),就可以造成一個XSS。 挖掘這種型別的漏洞等同於挖掘傳統的DOM XSS。
濫用Ref
Ref ( https://zh-hans.reactjs.org/docs/refs-and-the-dom.html ) 是React提供的 一種高階功能,允許開發者直接操作React元件渲染出來的DOM。React設計它的本意是實現動畫、或是和某些基於DOM的第三方庫配合使用(常見的如Prism等程式碼高亮庫)、或是對 video 等媒體標籤進行控制, 但一個API被設計出來是很難不被濫用的。
下圖展示了其中的一種濫用。這種濫用的挖掘和利用和常規的挖掘DOM XSS相同。只要值可控(該專案中不可控)也可以造成XSS。
由於React有幾個版本對Ref做了相當多的改動,因此在實際審計時看到的ref用法可能和圖中的不同,對挖掘DOM XSS無影響。
濫用dangerouslySetInnerHTML
某些時候,前端開發者需要直接往該標籤內寫入HTML。React希望開發者避免使用這種方式,特意給該API起了個又臭又長的名字,要求傳入的物件長成 {__html: 'HTML'} 的形式,還特意標註了個“dangerously”。 雖然他們為了防止濫用做出了很多努力,但似乎沒有起到什麼成效, dangerouslySetInnerHtml的濫用仍然非常常見。
如圖,一看就是使用者可控的XSS點。
直接全域性搜尋 dangerouslySetInnerHtml ,可以找到一個React專案的大多數XSS。
動態元件傳參/動態建立元件
我們看一下下列程式碼
const a = JSON.parse(location.hash.substr(1))
// hash = #{dangerouslySetInnerHTML:
{__html: '<script>alert(1)</script>'}}
return <div {...a} />
它和以下的ES5程式碼功能基本等價
var a = JSON.parse(location.hash.substr(1))
var b = {}
for (var key in a) {
// 此處存在原型鏈汙染,僅為示例
b[key] = a[key]}
return React.createElement({ "type": "div", "props": b})
這種將使用者輸入不限制地傳入屬性引數的做法顯然會導致XSS,一旦createElement的引數完全可控,實現完全使用者可控的動態元件建立,也可以直接導致XSS。
值得一提的是,react-dom會通過某些方法( https://github.com/facebook/react/blob/c88fb49d37fd01024e0a254a37b7810d107bdd1d/packages/react-dom/src/client/ReactDOMComponent.js#L395 )來防止動態建立的script標籤內的JS程式碼執行, 但是這個安全檢查繞過難度不高,可以直接用onerror等屬性替代。
特殊DOM標籤的特殊屬性
考慮以下程式碼:
const id = location.hash.substr(1)const a = <a href={id} />
const b = <iframe src={id} />
當遇到某些特殊DOM標籤的特殊屬性可控時,可以直接造成XSS。 由於開發者們一般情況下不會把onError 等事件讓使用者可控,即使可控React也不接受字串為引數, (報錯:Uncaught Error: Expected onError listener to be a function, instead got a value of string type.)
所以能考慮的只有類似 frame 、 iframe 、 a 、 meta 、 object 等較為特殊的標籤。`script`標籤的src和內容不可控無法造成XSS。
SSR時的可疑輸入
SSR是Server Side Render的縮寫,即伺服器端渲染。由於前端框架只工作在前端,導致百度等搜尋引擎無法對網站內容進行抓取,頁面首屏載入速度也同樣會有大幅度的降低。SSR技術可以解決這些問題。只要開發者編寫的JS程式碼對DOMAPI沒有依賴,這些程式碼就可以直接在Nodejs上執行,所以基於React的前端專案只需要將react-dom 置換為 react-dom/server 即可直接複用前端程式碼,在後端渲染頁面並直接輸出HTML。在這種開發模式下,前端與後端伺服器共用一套程式碼,在SSR的配合下DOMXSS可以轉化為儲存型、反射型等其他型別的XSS。
在後端渲染完成之後,前端需要基於後端的渲染結果繼續執行,所以後端在輸出HTML程式碼的同時也要將當前狀態返回給前端。這會涉及到物件的序列化與反序列化,會出現意料之外的安全問題。
如圖為Nextjs,現代最常見的SSR框架的實現。它會把所有的狀態寫入到 scriptid="__NEXT_DATA__" 內,前端程式碼會讀取這個標籤內的內容作出處理。
很多專案的SSR可能是迭代產生的新需求,因為Nextjs需要對專案結構進行相當大的改動,所以它們SSR部分有可能是自行開發的。Redux(一個狀態容器,通常與React配合使用)的官方網站提供了一個SSR的例子: https://redux.js.org/usage/server-rendering
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// https://redux.js.org/usage/server-rendering#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
/</g,
'\\u003c'
)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}
注意preloadState這個變數,它的值可以包含各種UGC內容,也有很多開發者會將react-router(和React配合使用的一個前端路由)的狀態和Redux同步。如果當前頁面存在訊息、微博等互動性功能,preloadState很有可能部分可控。正是因為這種原因,上述Redux的官方網站給出的程式碼加上了一層XSS過濾。
而如果開發者閱讀的文件不是Redux官方文件,而是一些更新較為遲緩的資料,則可能由於這些文件編寫者沒有安全意識而受到攻擊。如下圖的文件內的示例程式碼就缺少了XSS過濾。
原型鏈汙染
SSR
SSR時的前端和後端的程式碼絕大多數是共用的,因此可以通過審計前端程式碼的方式來對SSR服務進行攻擊。 如果前端打包時存在Sourcemap洩漏,就可以更直觀地看到具體的依賴庫,之後直接搜CVE或者npm audit。此處的原型鏈汙染想要RCE難度較大,一般的SSR框架,除了 express (Nodejs下的WebServer框架)以外,不怎麼依賴別的Nodejs平臺相關的庫和API。對於原型鏈汙染來說,部署最廣泛的可攻擊目標是模板引擎,但SSR一般不使用這些庫,導致攻擊面相對較小。
XSS
const a = <div {...props} />
考慮這種使用了ES6的Object spread語法來複制一個新物件的程式碼,原型鏈汙染在這種場合下無法觸發XSS。原因如下:
-
包括Babel和TypeScript的實現在內,按照標準,Object spread不會將原型鏈上的屬性複製到新物件上。
-
React自身在遍歷Object的每個屬性的時候,會使用
hasOwnProperty
檢查其是否是原型鏈屬性。
但同樣是ES6語法,Destructuring就不一樣了,如下程式碼
const {id, username, password} = props
等同於
var _props = props,
id = _props.id,
username = _props.username,
password = _props.password;
以上程式碼顯然可以被原型鏈汙染攻擊。這種寫法在現代前端程式碼中極為常見,我們以0CTF 2021 Final的useCTF()題為例。
這一題的官方解答的攻擊目標是 reapop 這個庫。以下是相關程式碼:
const {id, title, message, dismissible, showDismissButton, buttons, allowHTML, image} = notification
// ...
return (
<div>
<div style={metaStyles} className={classnames.notificationMeta}>
{title &&
(allowHTML ? (
<h4
style={titleStyles}
className={classnames.notificationTitle}
dangerouslySetInnerHTML={{__html: title}}
/>
) : (
<h4 style={titleStyles} className={classnames.notificationTitle}>
{title}
</h4>
))}
程式碼存在
dangerouslySetInnerHTML
,可以把這種危險引數作為原型鏈汙染的目的地。按此處的邏輯,只需要
Object.prototype.allowHTML
為
true
,頁面裡就會直接把
Object.prototype.title
屬性原樣輸出。
而來自俄羅斯的More Smoked Leet Chicken戰隊給出了更精妙的解法。這個題目的UI框架 chakra-ui 給部分元件提供了一個特殊的屬性
as
:
https://chakra-ui.com/docs/layout/box#as-prop
。該屬性的效果大致如下:
const a = <Box as="button" />
const b = <Box />
return <div>{a}{b}</div>
在DOM內會輸出為:
<div>
<button></button>
<div></div>
</div>
往下閱讀
as
的實現,程式碼在此處:
https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/styled/src/base.js#L134
const Styled: PrivateStyledComponent<Props> = withEmotionCache((props, cache, ref) => {
const finalTag = (shouldUseAs && props.as) || baseTag
for (let key in props) {
if (shouldUseAs && key === 'as') continue
if (finalShouldForwardProp(key)) {
newProps[key] = props[key]
}
}
newProps.className = className
newProps.ref = ref
const ele = React.createElement(finalTag, newProps)
這段程式碼有以下問題:
-
沒有檢查
props.as
是否屬於props物件(對於UI框架一般也沒有檢查的必要) -
finalTag = props.as
,即原型鏈汙染可控 -
在複製
props
到newProps
時未檢查屬性是否屬於props物件
因此通過原型鏈汙染可以完整控制一個React元件。參考本文“動態建立元件”一節,可以很輕鬆地構造出
<iframe src="javascript:alert(1)">
並。
as
屬性在幾乎所有UI框架中都存在,這使得一個原型鏈汙染漏洞可以在幾乎所有UI框架中造成XSS。(這題無法直接給Object設定一個
dangerouslySetInnerHTML
屬性,這會使其他程式碼無法執行)
as
屬性有點類似各種CMS的反序列化鏈,雖然不是漏洞,但這種feature會被原型鏈汙染漏洞濫用。
React Native?
因為React Native不使用瀏覽器渲染資料,所以不太可能出現XSS漏洞。挖掘RCE漏洞更好的方式是尋找 eval / new Function 等動態程式碼執行相關程式碼,或是尋找能呼叫某些Java / ObjectiveC API的地方。
後利用竊取資料
XSS不只是彈窗,後續利用也值得關注。獲取使用者資料是XSS漏洞的一大危害,而在React框架中獲取資料需要一些技巧。
最輕鬆的獲取資料的方法是從DOM或者是localStorage等資料展示/持久化儲存的地方獲取資料,但有些資料(例如Token)一般不會被渲染在頁面內,需要從React內部獲取這些資料。
在從React內部獲取資料之前,可以考慮通過Hook相關API的方式來獲取資料。對 fetch API和 XMLHttpRequest API進行hook (https://github.com/wendux/Ajax-hook ),或者是對資料附近的JavaScript/BOM/DOMAPI進行Hook,都是比較好實現又不依賴於React的通用解決方案。
如果實在難以獲得資料,必須從React內部獲得,則需要對React的相關概念進行學習。一個React專案的資料一般會儲存在這些地方:Prop、State、Context,或是ReduxStore。從外部很難獲取到React內部的值。
可以從React與外部互動的介面入手。React會在渲染出的DOM元素內增加一個屬性:
在引入了Fiber的React(16.8+),會多出 __reactFiber$xxxx 屬性,該屬性對應的就是這個DOM在React內部對應的FiberNode,可以直接使用child屬性獲得子節點。節點層級可以從React Dev Tool內檢視。通過讀取每個FiberNode的 memoizedProps 和 memoizedState ,即可直接獲取需要的Prop和State。 在高版本使用React Hooks的專案中,FiberNode的 memorizedState 是一個連結串列,該連結串列內的節點次序可以參考該元件原始碼內 useState 的呼叫順序。
舊版React,引入的屬性是 __reactInternalInstance 。State也是一個Object而非連結串列,可以方便地看到每個state的名字。
Context等屬性可以在該屬性內的 stateNode 屬性找到, 對於Redux只需要找到需要的資料在哪個React節點內被呼叫,讀取其props/state也可以間接獲取內部資料。獲取這些資料最主要的麻煩是如何尋找到對應的ReactDOM節點,這需要配合Dev Tool和原始碼慢慢挖掘。
自查React專案的安全問題
-
排查所有用到了 dangerouslySetInnerHtml 的元件,並充分論證此處使用該API的必要性。儘量改寫為使用JSX的方式。
-
排查所有的 useRef 、 refs 等涉及到Ref API使用的元件,並儘量規避其的使用。
-
排查所有的DOM API呼叫(關鍵詞包括 appendChildinner/HTML 等),將程式碼儘量改寫為不依賴DOM的形式。
-
排查SSR的資料同步部分,對使用者輸入進行過濾。
-
使用 npm audit 排查是否有某些第三方依賴存在漏洞。
-
自查原型鏈汙染漏洞。
擴充套件閱讀
codeql挖掘React應用的XSS實踐 ( https://hexo.imagemlt.xyz/post/javascript-codeql-learning/index.html )
騰訊安全科恩實驗室
關注我們,KeenLab Tech Talk系列將持續以影片/文字形式輸出技術乾貨,涵蓋: 漏洞分析、工具分享、技巧總結、演算法優化 、入門引導 、賽題講解 等安全領域主題。更有不定期 抽獎活動 等你參與!