【React】826- 淺談 React 中的 XSS 攻擊
本文首發(fā)于政采云前端團(tuán)隊(duì)博客:淺談 React 中的 XSS 攻擊
https://www.zoo.team/article/xss-in-react

前言
前端一般會(huì)面臨 XSS 這樣的安全風(fēng)險(xiǎn),但隨著 React 等現(xiàn)代前端框架的流行,使我們在平時(shí)開發(fā)時(shí)不用太關(guān)注安全問題。以 React 為例,React 從設(shè)計(jì)層面上就具備了很好的防御 XSS 的能力。本文將以源碼角度,看看 React 做了哪些事情來實(shí)現(xiàn)這種安全性的。
XSS 攻擊是什么
Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。XSS 攻擊通常指的是利用網(wǎng)頁的漏洞,攻擊者通過巧妙的方法注入 XSS 代碼到網(wǎng)頁,因?yàn)闉g覽器無法分辨哪些腳本是可信的,導(dǎo)致 XSS 腳本被執(zhí)行。XSS 腳本通常能夠竊取用戶數(shù)據(jù)并發(fā)送到攻擊者的網(wǎng)站,或者冒充用戶,調(diào)用目標(biāo)網(wǎng)站接口并執(zhí)行攻擊者指定的操作。
XSS 攻擊類型
反射型 XSS
XSS 腳本來自當(dāng)前 HTTP 請求 當(dāng)服務(wù)器在 HTTP 請求中接收數(shù)據(jù)并將該數(shù)據(jù)拼接在 HTML 中返回時(shí),例子:
//?某網(wǎng)站具有搜索功能,該功能通過 URL 參數(shù)接收用戶提供的搜索詞:
https://xxx.com/search?query=123
//?服務(wù)器在對此 URL 的響應(yīng)中回顯提供的搜索詞:
<p>您搜索的是:?123p>
//?如果服務(wù)器不對數(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>
//?如果有用戶請求攻擊者的 URL ,則攻擊者提供的腳本將在用戶的瀏覽器中執(zhí)行。
存儲(chǔ)型 XSS
XSS 腳本來自服務(wù)器數(shù)據(jù)庫中 攻擊者將惡意代碼提交到目標(biāo)網(wǎng)站的數(shù)據(jù)庫中,普通用戶訪問網(wǎng)站時(shí)服務(wù)器將惡意代碼返回,瀏覽器默認(rèn)執(zhí)行,例子:
//?某個(gè)評論頁,能查看用戶評論。
//?攻擊者將惡意代碼當(dāng)做評論提交,服務(wù)器沒對數(shù)據(jù)進(jìn)行轉(zhuǎn)義等處理
//?評論輸入:
<textarea>
??<img?src=""?onerror?="alert('xss')">
textarea>
//?則攻擊者提供的腳本將在所有訪問該評論頁的用戶瀏覽器執(zhí)行
DOM 型 XSS
該漏洞存在于客戶端代碼,與服務(wù)器無關(guān)
類似反射型,區(qū)別在于 DOM 型 XSS 并不會(huì)和后臺(tái)進(jìn)行交互,前端直接將 URL 中的數(shù)據(jù)不做處理并動(dòng)態(tài)插入到 HTML 中,是純粹的前端安全問題,要做防御也只能在客戶端上進(jìn)行防御。
React 如何防止 XSS 攻擊
無論使用哪種攻擊方式,其本質(zhì)就是將惡意代碼注入到應(yīng)用中,瀏覽器去默認(rèn)執(zhí)行。React 官方中提到了 React DOM 在渲染所有輸入內(nèi)容之前,默認(rèn)會(huì)進(jìn)行轉(zhuǎn)義。它可以確保在你的應(yīng)用中,永遠(yuǎn)不會(huì)注入那些并非自己明確編寫的內(nèi)容。所有的內(nèi)容在渲染之前都被轉(zhuǎn)換成了字符串,因此惡意代碼無法成功注入,從而有效地防止了 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)義,可以看到對瀏覽器有特殊含義的字符都被轉(zhuǎn)義了,惡意代碼在渲染到 HTML 前都被轉(zhuǎn)成了字符串,如下:
//?一段惡意代碼
""?onerror?="alert('xss')">?
//?轉(zhuǎn)義后輸出到?html?中
?
這樣就有效的防止了 XSS 攻擊。
JSX 語法
JSX 實(shí)際上是一種語法糖,Babel 會(huì)把 JSX 編譯成 React.createElement() 的函數(shù)調(diào)用,最終返回一個(gè) ReactElement,以下為這幾個(gè)步驟對應(yīng)的代碼:
//?JSX
const?element?=?(
??<h1?className="greeting">
????Hello,?world!
??h1>
);
//?通過?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 的原理后,我們來試試能否通過構(gòu)造特殊的 Children 進(jìn)行 XSS 注入,來看下面一段代碼:
const?storedData?=?`{
??"ref":null,
??"type":"body",
??"props":{
??"dangerouslySetInnerHTML":{
??"__html":""
??????}
??}
}`;
//?轉(zhuǎn)成?JSON
const?parsedData?=?JSON.parse(storedData);
//?將數(shù)據(jù)渲染到頁面
render?()?{
??return?<span>?{parsedData}?span>;?
}
這段代碼中, 運(yùn)行后會(huì)報(bào)以下錯(cuò)誤,提示不是有效的 ReactChild
Uncaught?(in?promise)?Error:?Objects?are?not?valid?as?a?React?child?(found:?object?with?keys?{ref,?type,?props}).?If?you?meant?to?render?a?collection?of?children,?use?an?array?instead.
那究竟是哪里出問題了?我們看一下 ReactElement 的源碼:
const?symbolFor?=?Symbol.for;
REACT_ELEMENT_TYPE?=?symbolFor('react.element');
const?ReactElement?=?function(type,?key,?ref,?self,?source,?owner,?props)?{
??const?element?=?{
????//?這個(gè)?tag?唯一標(biāo)識了此為?ReactElement
????$$typeof:?REACT_ELEMENT_TYPE,
????//?元素的內(nèi)置屬性
????type:?type,
????key:?key,
????ref:?ref,
????props:?props,
????//?記錄創(chuàng)建此元素的組件
????_owner:?owner,
??};
??...
??return?element;
}
注意到其中有個(gè)屬性是 $$typeof,它是用來標(biāo)記此對象是一個(gè) ReactElement,React 在進(jìn)行渲染前會(huì)通過此屬性進(jìn)行校驗(yàn),校驗(yàn)不通過將會(huì)拋出上面的錯(cuò)誤。React 利用這個(gè)屬性來防止通過構(gòu)造特殊的 Children 來進(jìn)行的 XSS 攻擊,原因是 $$typeof 是個(gè) Symbol 類型,進(jìn)行 JSON 轉(zhuǎn)換后會(huì) Symbol 值會(huì)丟失,無法在前后端進(jìn)行傳輸。如果用戶提交了特殊的 Children,也無法進(jìn)行渲染,利用此特性,可以防止存儲(chǔ)型的 XSS 攻擊。
在 React 中可引起漏洞的一些寫法
使用 dangerouslySetInnerHTML
dangerouslySetInnerHTML 是 React 為瀏覽器 DOM 提供 innerHTML 的替換方案。通常來講,使用代碼直接設(shè)置 HTML 存在風(fēng)險(xiǎn),因?yàn)楹苋菀资褂脩舯┞对?XSS 攻擊下,因?yàn)楫?dāng)使用 dangerouslySetInnerHTML 時(shí),React 將不會(huì)對輸入進(jìn)行任何處理并直接渲染到 HTML 中,如果攻擊者在 dangerouslySetInnerHTML 傳入了惡意代碼,那么瀏覽器將會(huì)運(yùn)行惡意代碼??聪略创a:
function?getNonChildrenInnerMarkup(props)?{
??const?innerHTML?=?props.dangerouslySetInnerHTML;?//?有dangerouslySetInnerHTML屬性,會(huì)不經(jīng)轉(zhuǎn)義就渲染__html的內(nèi)容
??if?(innerHTML?!=?null)?{
????if?(innerHTML.__html?!=?null)?{
??????return?innerHTML.__html;
????}
??}?else?{
????const?content?=?props.children;
????if?(typeof?content?===?'string'?||?typeof?content?===?'number')?{
??????return?escapeTextForBrowser(content);
????}
??}
??return?null;
}
所以平時(shí)開發(fā)時(shí)最好避免使用 dangerouslySetInnerHTML,如果不得不使用的話,前端或服務(wù)端必須對輸入進(jìn)行相關(guān)驗(yàn)證,例如對特殊輸入進(jìn)行過濾、轉(zhuǎn)義等處理。前端這邊處理的話,推薦使用白名單過濾?(https://jsxss.com/zh/index.html),通過白名單控制允許的 HTML 標(biāo)簽及各標(biāo)簽的屬性。
通過用戶提供的對象來創(chuàng)建 React 組件
舉個(gè)例子:
//?用戶的輸入
const?userProvidePropsString?=?`{"dangerouslySetInnerHTML":{"__html":""}}"`;
//?經(jīng)過?JSON?轉(zhuǎn)換
const?userProvideProps?=?JSON.parse(userProvidePropsString);
//?userProvideProps?=?{
//???dangerouslySetInnerHTML:?{
//?????"__html":?``
//??????}
//?};
render()?{
?????//?出于某種原因解析用戶提供的?JSON?并將對象作為?props?傳遞
????return?<div?{...userProvideProps}?/>?
}
這段代碼將用戶提供的數(shù)據(jù)進(jìn)行 JSON 轉(zhuǎn)換后直接當(dāng)做 div 的屬性,當(dāng)用戶構(gòu)造了類似例子中的特殊字符串時(shí),頁面就會(huì)被注入惡意代碼,所以要注意平時(shí)在開發(fā)中不要直接使用用戶的輸入作為屬性。
使用用戶輸入的值來渲染 a 標(biāo)簽的 href 屬性,或類似 img 標(biāo)簽的 src 屬性等
const?userWebsite?=?"javascript:alert('xss');";
<a?href={userWebsite}>a>
如果沒有對該 URL 進(jìn)行過濾以防止通過 javascript: 或 data: 來執(zhí)行 JavaScript,則攻擊者可以構(gòu)造 XSS 攻擊,此處會(huì)有潛在的安全問題。用戶提供的 URL 需要在前端或者服務(wù)端在入庫之前進(jìn)行驗(yàn)證并過濾。
服務(wù)端如何防止 XSS 攻擊
服務(wù)端作為最后一道防線,也需要做一些措施以防止 XSS 攻擊,一般涉及以下幾方面:
在接收到用戶輸入時(shí),需要對輸入進(jìn)行盡可能嚴(yán)格的過濾,過濾或移除特殊的 HTML 標(biāo)簽、JS 事件的關(guān)鍵字等。 在輸出時(shí)對數(shù)據(jù)進(jìn)行轉(zhuǎn)義,根據(jù)輸出語境 (html/javascript/css/url),進(jìn)行對應(yīng)的轉(zhuǎn)義 對關(guān)鍵 Cookie 設(shè)置 http-only 屬性,JS腳本就不能訪問到 http-only 的 Cookie 了 利用 CSP (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP) 來抵御或者削弱 XSS 攻擊,一個(gè) CSP 兼容的瀏覽器將會(huì)僅執(zhí)行從白名單域獲取到的腳本文件,忽略所有的其他腳本 (包括內(nèi)聯(lián)腳本和 HTML 的事件處理屬性)
總結(jié)
出現(xiàn) XSS 漏洞本質(zhì)上是輸入輸出驗(yàn)證不充分,React 在設(shè)計(jì)上已經(jīng)很安全了,但是一些反模式的寫法還是會(huì)引起安全漏洞。Vue 也是類似,Vue 做的安全措施主要也是轉(zhuǎn)義,HTML 的內(nèi)容和動(dòng)態(tài)綁定的屬性都會(huì)進(jìn)行轉(zhuǎn)義。無論使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻擊,所以服務(wù)端必須對前端參數(shù)做一些驗(yàn)證,包括但不限于特殊字符轉(zhuǎn)義、標(biāo)簽、屬性白名單過濾等。一旦出現(xiàn)安全問題一般都是挺嚴(yán)重的,不管是敏感數(shù)據(jù)被竊取或者用戶資金被盜,損失往往無法挽回。我們平時(shí)開發(fā)中需要保持安全意識,保持代碼的可靠性和安全性。
小游戲
看完文章可以嘗試下 XSS 的?小游戲?(https://xss-game.appspot.com/),自己動(dòng)手實(shí)踐模擬 XSS 攻擊,可以對 XSS 有更進(jìn)一步的認(rèn)識。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 100+ 篇原創(chuàng)文章
