淺談 React 中的 XSS 攻擊

70 篇原創好文~
本文首發於政採雲前端團隊博客: 淺談 React 中的 XSS 攻擊

前言

前端通常會面臨 XSS 這樣的安全風險,但隨着 React 等現代前端框架的流行,使咱們在平時開發時不用太關注安全問題。以 React 爲例,React 從設計層面上就具有了很好的防護 XSS 的能力。本文將以源碼角度,看看 React 作了哪些事情來實現這種安全性的。javascript

XSS 攻擊是什麼

Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。XSS 攻擊一般指的是利用網頁的漏洞,攻擊者經過巧妙的方法注入 XSS 代碼到網頁,由於瀏覽器沒法分辨哪些腳本是可信的,致使 XSS 腳本被執行。XSS 腳本一般可以竊取用戶數據併發送到攻擊者的網站,或者冒充用戶,調用目標網站接口並執行攻擊者指定的操做。css

XSS 攻擊類型

反射型 XSS

  • XSS 腳原本自當前 HTTP 請求
  • 當服務器在 HTTP 請求中接收數據並將該數據拼接在 HTML 中返回時,例子:
// 某網站具備搜索功能,該功能經過 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 ,則攻擊者提供的腳本將在用戶的瀏覽器中執行。

存儲型 XSS

  • XSS 腳原本自服務器數據庫中
  • 攻擊者將惡意代碼提交到目標網站的數據庫中,普通用戶訪問網站時服務器將惡意代碼返回,瀏覽器默認執行,例子:
// 某個評論頁,能查看用戶評論。
   // 攻擊者將惡意代碼當作評論提交,服務器沒對數據進行轉義等處理
   // 評論輸入:
   <textarea>
      <img src="empty.png" onerror ="alert('xss')">
   </textarea>
   // 則攻擊者提供的腳本將在全部訪問該評論頁的用戶瀏覽器執行

DOM 型 XSS

該漏洞存在於客戶端代碼,與服務器無關html

  • 相似反射型,區別在於 DOM 型 XSS 並不會和後臺進行交互,前端直接將 URL 中的數據不作處理並動態插入到 HTML 中,是純粹的前端安全問題,要作防護也只能在客戶端上進行防護。

React 如何防止 XSS 攻擊

不管使用哪一種攻擊方式,其本質就是將惡意代碼注入到應用中,瀏覽器去默認執行。React 官方中提到了 React DOM 在渲染全部輸入內容以前,默認會進行轉義。它能夠確保在你的應用中,永遠不會注入那些並不是本身明確編寫的內容。全部的內容在渲染以前都被轉換成了字符串,所以惡意代碼沒法成功注入,從而有效地防止了 XSS 攻擊。咱們具體看下:前端

自動轉義

React 在渲染 HTML 內容和渲染 DOM 屬性時都會將 "'&<> 這幾個字符進行轉義,轉義部分源碼以下:java

for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // "
        escape = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;';
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }
  }

這段代碼是 React 在渲染到瀏覽器前進行的轉義,能夠看到對瀏覽器有特殊含義的字符都被轉義了,惡意代碼在渲染到 HTML 前都被轉成了字符串,以下:react

// 一段惡意代碼
<img src="empty.png" onerror ="alert('xss')"> 
// 轉義後輸出到 html 中
&lt;img src=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt;

這樣就有效的防止了 XSS 攻擊。數據庫

JSX 語法

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 攻擊。

在 React 中可引發漏洞的一些寫法

使用 dangerouslySetInnerHTML

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 標籤及各標籤的屬性。

經過用戶提供的對象來建立 React 組件

舉個例子:

// 用戶的輸入
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 的屬性,當用戶構造了相似例子中的特殊字符串時,頁面就會被注入惡意代碼,因此要注意平時在開發中不要直接使用用戶的輸入做爲屬性。

使用用戶輸入的值來渲染 a 標籤的 href 屬性,或相似 img 標籤的 src 屬性等

const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>

若是沒有對該 URL 進行過濾以防止經過 javascript:data: 來執行 JavaScript,則攻擊者能夠構造 XSS 攻擊,此處會有潛在的安全問題。
用戶提供的 URL 須要在前端或者服務端在入庫以前進行驗證並過濾。

服務端如何防止 XSS 攻擊

服務端做爲最後一道防線,也須要作一些措施以防止 XSS 攻擊,通常涉及如下幾方面:

  • 在接收到用戶輸入時,須要對輸入進行儘量嚴格的過濾,過濾或移除特殊的 HTML 標籤、JS 事件的關鍵字等。
  • 在輸出時對數據進行轉義,根據輸出語境 (html/javascript/css/url),進行對應的轉義
  • 對關鍵 Cookie 設置 http-only 屬性,JS 腳本就不能訪問到 http-only 的 Cookie 了
  • 利用 CSP 來抵禦或者削弱 XSS 攻擊,一個 CSP 兼容的瀏覽器將會僅執行從白名單域獲取到的腳本文件,忽略全部的其餘腳本 (包括內聯腳本和 HTML 的事件處理屬性)

總結

出現 XSS 漏洞本質上是輸入輸出驗證不充分,React 在設計上已經很安全了,可是一些反模式的寫法仍是會引發安全漏洞。Vue 也是相似,Vue 作的安全措施主要也是轉義,HTML 的內容和動態綁定的屬性都會進行轉義。不管使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻擊,因此服務端必須對前端參數作一些驗證,包括但不限於特殊字符轉義、標籤、屬性白名單過濾等。一旦出現安全問題通常都是挺嚴重的,不論是敏感數據被竊取或者用戶資金被盜,損失每每沒法挽回。咱們平時開發中須要保持安全意識,保持代碼的可靠性和安全性。

小遊戲

看完文章能夠嘗試下 XSS 的小遊戲,本身動手實踐模擬 XSS 攻擊,能夠對 XSS 有更進一步的認識。

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索