React 怎么實(shí)現(xiàn)預(yù)防XSS 攻擊的
本文首發(fā)于政采云前端團(tuán)隊(duì)博客:淺談 React 中的 XSS 攻擊
https://www.zoo.team/article/xss-in-react

前言
前端一般會(huì)面臨 XSS 這樣的安全風(fēng)險(xiǎn),但隨著 React 等現(xiàn)代前端框架的流行,使我們?cè)谄綍r(shí)開(kāi)發(fā)時(shí)不用太關(guān)注安全問(wèn)題。以 React 為例,React 從設(shè)計(jì)層面上就具備了很好的防御 XSS 的能力。本文將以源碼角度,看看 React 做了哪些事情來(lái)實(shí)現(xiàn)這種安全性的。
XSS 攻擊是什么
Cross-Site Scripting(跨站腳本攻擊)簡(jiǎn)稱 XSS,是一種代碼注入攻擊。XSS 攻擊通常指的是利用網(wǎng)頁(yè)的漏洞,攻擊者通過(guò)巧妙的方法注入 XSS 代碼到網(wǎng)頁(yè),因?yàn)闉g覽器無(wú)法分辨哪些腳本是可信的,導(dǎo)致 XSS 腳本被執(zhí)行。XSS 腳本通常能夠竊取用戶數(shù)據(jù)并發(fā)送到攻擊者的網(wǎng)站,或者冒充用戶,調(diào)用目標(biāo)網(wǎng)站接口并執(zhí)行攻擊者指定的操作。
XSS 攻擊類型
反射型 XSS
XSS 腳本來(lái)自當(dāng)前 HTTP 請(qǐng)求 當(dāng)服務(wù)器在 HTTP 請(qǐng)求中接收數(shù)據(jù)并將該數(shù)據(jù)拼接在 HTML 中返回時(shí),例子:
//?某網(wǎng)站具有搜索功能,該功能通過(guò) URL 參數(shù)接收用戶提供的搜索詞:
https://xxx.com/search?query=123
//?服務(wù)器在對(duì)此 URL 的響應(yīng)中回顯提供的搜索詞:
<p>您搜索的是:?123p>
//?如果服務(wù)器不對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)義等處理,則攻擊者可以構(gòu)造如下鏈接進(jìn)行攻擊:
https://xxx.com/search?query=
//?該 URL 將導(dǎo)致以下響應(yīng),并運(yùn)行 alert('xss'):
<p>您搜索的是:?<img?src=""?onerror?="alert('xss')">p>
//?如果有用戶請(qǐng)求攻擊者的 URL ,則攻擊者提供的腳本將在用戶的瀏覽器中執(zhí)行。
存儲(chǔ)型 XSS
XSS 腳本來(lái)自服務(wù)器數(shù)據(jù)庫(kù)中 攻擊者將惡意代碼提交到目標(biāo)網(wǎng)站的數(shù)據(jù)庫(kù)中,普通用戶訪問(wèn)網(wǎng)站時(shí)服務(wù)器將惡意代碼返回,瀏覽器默認(rèn)執(zhí)行,例子:
//?某個(gè)評(píng)論頁(yè),能查看用戶評(píng)論。
//?攻擊者將惡意代碼當(dāng)做評(píng)論提交,服務(wù)器沒(méi)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)義等處理
//?評(píng)論輸入:
<textarea>
??<img?src=""?onerror?="alert('xss')">
textarea>
//?則攻擊者提供的腳本將在所有訪問(wèn)該評(píng)論頁(yè)的用戶瀏覽器執(zhí)行
DOM 型 XSS
該漏洞存在于客戶端代碼,與服務(wù)器無(wú)關(guān)
類似反射型,區(qū)別在于 DOM 型 XSS 并不會(huì)和后臺(tái)進(jìn)行交互,前端直接將 URL 中的數(shù)據(jù)不做處理并動(dòng)態(tài)插入到 HTML 中,是純粹的前端安全問(wèn)題,要做防御也只能在客戶端上進(jìn)行防御。
React 如何防止 XSS 攻擊
無(wú)論使用哪種攻擊方式,其本質(zhì)就是將惡意代碼注入到應(yīng)用中,瀏覽器去默認(rèn)執(zhí)行。React 官方中提到了 React DOM 在渲染所有輸入內(nèi)容之前,默認(rèn)會(huì)進(jìn)行轉(zhuǎn)義。它可以確保在你的應(yīng)用中,永遠(yuǎn)不會(huì)注入那些并非自己明確編寫(xiě)的內(nèi)容。所有的內(nèi)容在渲染之前都被轉(zhuǎn)換成了字符串,因此惡意代碼無(wú)法成功注入,從而有效地防止了 XSS 攻擊。我們具體看下:
自動(dòng)轉(zhuǎn)義
React 在渲染 HTML 內(nèi)容和渲染 DOM 屬性時(shí)都會(huì)將 "'&<> 這幾個(gè)字符進(jìn)行轉(zhuǎn)義,轉(zhuǎn)義部分源碼如下:
for?(index?=?match.index;?index???switch?(str.charCodeAt(index))?{
????case?34:?//?"
??????escape?=?'"';
??????break;
????case?38:?//?&
??????escape?=?'&';
??????break;
????case?39:?//?'
??????escape?=?''';
??????break;
????case?60:?//?<
??????escape?=?'<';
??????break;
????case?62:?//?>
??????escape?=?'>';
??????break;
????default:
??????continue;
????}
??}
這段代碼是 React 在渲染到瀏覽器前進(jìn)行的轉(zhuǎn)義,可以看到對(duì)瀏覽器有特殊含義的字符都被轉(zhuǎn)義了,惡意代碼在渲染到 HTML 前都被轉(zhuǎn)成了字符串,如下:
//?一段惡意代碼
""?onerror?="alert('xss')">?
//?轉(zhuǎn)義后輸出到?html?中
?
這樣就有效的防止了 XSS 攻擊。
JSX 語(yǔ)法
JSX 實(shí)際上是一種語(yǔ)法糖,Babel 會(huì)把 JSX 編譯成 React.createElement() 的函數(shù)調(diào)用,最終返回一個(gè) ReactElement,以下為這幾個(gè)步驟對(duì)應(yīng)的代碼:
//?JSX
const?element?=?(
??<h1?className="greeting">
????Hello,?world!
??h1>
);
//?通過(guò)?babel?編譯后的代碼
const?element?=?React.createElement(
??'h1',
??{className:?'greeting'},
??'Hello,?world!'
);
//?React.createElement()?方法返回的?ReactElement
const?element?=?{
??$$typeof:?Symbol('react.element'),
??type:?'h1',
??key:?null,
??props:?{
????children:?'Hello,?world!',
??????className:?'greeting'???
??}
??...
}
我們可以看到,最終渲染的內(nèi)容是在 Children 屬性中,那了解了 JSX 的原理后,我們來(lái)試試能否通過(guò)構(gòu)造特殊的 Children 進(jìn)行 XSS 注入,來(lái)看下面一段代碼:
const?storedData?=?`{
??"ref":null,
??"type":"body",
??"props":{
??"dangerouslySetInnerHTML":{
??"__html":"