第 70 篇原創好文~
本文首發於政採雲前端團隊博客: 淺談 React 中的 XSS 攻擊
![]()
前端通常會面臨 XSS 這樣的安全風險,但隨着 React 等現代前端框架的流行,使咱們在平時開發時不用太關注安全問題。以 React 爲例,React 從設計層面上就具有了很好的防護 XSS 的能力。本文將以源碼角度,看看 React 作了哪些事情來實現這種安全性的。javascript
Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。XSS 攻擊一般指的是利用網頁的漏洞,攻擊者經過巧妙的方法注入 XSS 代碼到網頁,由於瀏覽器沒法分辨哪些腳本是可信的,致使 XSS 腳本被執行。XSS 腳本一般可以竊取用戶數據併發送到攻擊者的網站,或者冒充用戶,調用目標網站接口並執行攻擊者指定的操做。css
// 某網站具備搜索功能,該功能經過 URL 參數接收用戶提供的搜索詞: https://xxx.com/search?query=123 // 服務器在對此 URL 的響應中回顯提供的搜索詞: <p>您搜索的是: 123</p> // 若是服務器不對數據進行轉義等處理,則攻擊者能夠構造以下連接進行攻擊: https://xxx.com/search?query=<img src="empty.png" onerror ="alert('xss')"> // 該 URL 將致使如下響應,並運行 alert('xss'): <p>您搜索的是: <img src="empty.png" onerror ="alert('xss')"></p> // 若是有用戶請求攻擊者的 URL ,則攻擊者提供的腳本將在用戶的瀏覽器中執行。
// 某個評論頁,能查看用戶評論。 // 攻擊者將惡意代碼當作評論提交,服務器沒對數據進行轉義等處理 // 評論輸入: <textarea> <img src="empty.png" onerror ="alert('xss')"> </textarea> // 則攻擊者提供的腳本將在全部訪問該評論頁的用戶瀏覽器執行
該漏洞存在於客戶端代碼,與服務器無關html
不管使用哪一種攻擊方式,其本質就是將惡意代碼注入到應用中,瀏覽器去默認執行。React 官方中提到了 React DOM 在渲染全部輸入內容以前,默認會進行轉義。它能夠確保在你的應用中,永遠不會注入那些並不是本身明確編寫的內容。全部的內容在渲染以前都被轉換成了字符串,所以惡意代碼沒法成功注入,從而有效地防止了 XSS 攻擊。咱們具體看下:前端
React 在渲染 HTML 內容和渲染 DOM 屬性時都會將 "'&<>
這幾個字符進行轉義,轉義部分源碼以下:java
for (index = match.index; index < str.length; 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 在渲染到瀏覽器前進行的轉義,能夠看到對瀏覽器有特殊含義的字符都被轉義了,惡意代碼在渲染到 HTML 前都被轉成了字符串,以下:react
// 一段惡意代碼 <img src="empty.png" onerror ="alert('xss')"> // 轉義後輸出到 html 中 <img src="empty.png" onerror ="alert('xss')">
這樣就有效的防止了 XSS 攻擊。數據庫
JSX 其實是一種語法糖,Babel 會把 JSX 編譯成 React.createElement()
的函數調用,最終返回一個 ReactElement
,如下爲這幾個步驟對應的代碼:後端
// 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' } ... }
咱們能夠看到,最終渲染的內容是在 Children 屬性中,那瞭解了 JSX 的原理後,咱們來試試可否經過構造特殊的 Children 進行 XSS 注入,來看下面一段代碼:promise
const storedData = `{ "ref":null, "type":"body", "props":{ "dangerouslySetInnerHTML":{ "__html":"<img src=\"empty.png\" onerror =\"alert('xss')\"/>" } } }`; // 轉成 JSON const parsedData = JSON.parse(storedData); // 將數據渲染到頁面 render () { return <span> {parsedData} </span>; }
這段代碼中, 運行後會報如下錯誤,提示不是有效的 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 = { // 這個 tag 惟一標識了此爲 ReactElement $$typeof: REACT_ELEMENT_TYPE, // 元素的內置屬性 type: type, key: key, ref: ref, props: props, // 記錄建立此元素的組件 _owner: owner, }; ... return element; }
注意到其中有個屬性是 $$typeof`,它是用來標記此對象是一個 `ReactElement`,React 在進行渲染前會經過此屬性進行校驗,校驗不經過將會拋出上面的錯誤。React 利用這個屬性來防止經過構造特殊的 Children 來進行的 XSS 攻擊,緣由是 `$$typeof
是個 Symbol 類型,進行 JSON 轉換後會 Symbol 值會丟失,沒法在先後端進行傳輸。若是用戶提交了特殊的 Children,也沒法進行渲染,利用此特性,能夠防止存儲型的 XSS 攻擊。
dangerouslySetInnerHTML
是 React 爲瀏覽器 DOM 提供 innerHTML
的替換方案。一般來說,使用代碼直接設置 HTML 存在風險,由於很容易使用戶暴露在 XSS 攻擊下,由於當使用 dangerouslySetInnerHTML
時,React 將不會對輸入進行任何處理並直接渲染到 HTML 中,若是攻擊者在 dangerouslySetInnerHTML 傳入了惡意代碼,那麼瀏覽器將會運行惡意代碼。看下源碼:
function getNonChildrenInnerMarkup(props) { const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML屬性,會不經轉義就渲染__html的內容 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; }
因此平時開發時最好避免使用 dangerouslySetInnerHTML
,若是不得不使用的話,前端或服務端必須對輸入進行相關驗證,例如對特殊輸入進行過濾、轉義等處理。前端這邊處理的話,推薦使用白名單過濾,經過白名單控制容許的 HTML 標籤及各標籤的屬性。
舉個例子:
// 用戶的輸入 const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' src='empty.png' />"}}"`; // 通過 JSON 轉換 const userProvideProps = JSON.parse(userProvidePropsString); // userProvideProps = { // dangerouslySetInnerHTML: { // "__html": `<img onerror='alert("xss");' src='empty.png' />` // } // }; render() { // 出於某種緣由解析用戶提供的 JSON 並將對象做爲 props 傳遞 return <div {...userProvideProps} /> }
這段代碼將用戶提供的數據進行 JSON 轉換後直接當作 div
的屬性,當用戶構造了相似例子中的特殊字符串時,頁面就會被注入惡意代碼,因此要注意平時在開發中不要直接使用用戶的輸入做爲屬性。
const userWebsite = "javascript:alert('xss');"; <a href={userWebsite}></a>
若是沒有對該 URL 進行過濾以防止經過 javascript:
或 data:
來執行 JavaScript,則攻擊者能夠構造 XSS 攻擊,此處會有潛在的安全問題。
用戶提供的 URL 須要在前端或者服務端在入庫以前進行驗證並過濾。
服務端做爲最後一道防線,也須要作一些措施以防止 XSS 攻擊,通常涉及如下幾方面:
出現 XSS 漏洞本質上是輸入輸出驗證不充分,React 在設計上已經很安全了,可是一些反模式的寫法仍是會引發安全漏洞。Vue 也是相似,Vue 作的安全措施主要也是轉義,HTML 的內容和動態綁定的屬性都會進行轉義。不管使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻擊,因此服務端必須對前端參數作一些驗證,包括但不限於特殊字符轉義、標籤、屬性白名單過濾等。一旦出現安全問題通常都是挺嚴重的,不論是敏感數據被竊取或者用戶資金被盜,損失每每沒法挽回。咱們平時開發中須要保持安全意識,保持代碼的可靠性和安全性。
看完文章能夠嘗試下 XSS 的小遊戲,本身動手實踐模擬 XSS 攻擊,能夠對 XSS 有更進一步的認識。
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com