作者:陳吉
轉發鏈接:https://mp.weixin.qq.com/s/HweEFh78WXLawyQr_Vsl5G
前言
前端一般會面臨 XSS 這樣的安全風險,但隨著 React 等現代前端框架的流行,使我們在平時開發時不用太關注安全問題。以 React 為例,React 從設計層面上就具備了很好的防御 XSS 的能力。本文將以源碼角度,看看 React 做了哪些事情來實現這種安全性的。
XSS 攻擊是什么
Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。XSS 攻擊通常指的是利用網頁的漏洞,攻擊者通過巧妙的方法注入 XSS 代碼到網頁,因為瀏覽器無法分辨哪些腳本是可信的,導致 XSS 腳本被執行。XSS 腳本通常能夠竊取用戶數據并發送到攻擊者的網站,或者冒充用戶,調用目標網站接口并執行攻擊者指定的操作。
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
該漏洞存在于客戶端代碼,與服務器無關
- 類似反射型,區別在于 DOM 型 XSS 并不會和后臺進行交互,前端直接將 URL 中的數據不做處理并動態插入到 HTML 中,是純粹的前端安全問題,要做防御也只能在客戶端上進行防御。
React 如何防止 XSS 攻擊
無論使用哪種攻擊方式,其本質就是將惡意代碼注入到應用中,瀏覽器去默認執行。React 官方中提到了 React DOM 在渲染所有輸入內容之前,默認會進行轉義。它可以確保在你的應用中,永遠不會注入那些并非自己明確編寫的內容。所有的內容在渲染之前都被轉換成了字符串,因此惡意代碼無法成功注入,從而有效地防止了 XSS 攻擊。我們具體看下:
自動轉義
React 在渲染 HTML 內容和渲染 DOM 屬性時都會將 "'&<> 這幾個字符進行轉義,轉義部分源碼如下:
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 全都被轉成了字符串,如下:
// 一段惡意代碼
<img src="empty.png" onerror ="alert('xss')">
// 轉義后輸出到 html 中<img src="empty.png" onerror ="alert('xss')">
這樣就有效的防止了 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 注入,來看下面一段代碼:
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,如果不得不使用的話,前端或服務端必須對輸入進行相關驗證,例如對特殊輸入進行過濾、轉義等處理。前端這邊處理的話,推薦使用白名單過濾 (https://jsxss.com/zh/index.html),通過白名單控制允許的 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 (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP) 來抵御或者削弱 XSS 攻擊,一個 CSP 兼容的瀏覽器將會僅執行從白名單域獲取到的腳本文件,忽略所有的其他腳本 (包括內聯腳本和 HTML 的事件處理屬性)
總結
出現 XSS 漏洞本質上是輸入輸出驗證不充分,React 在設計上已經很安全了,但是一些反模式的寫法還是會引起安全漏洞。Vue 也是類似,Vue 做的安全措施主要也是轉義,HTML 的內容和動態綁定的屬性都會進行轉義。無論使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻擊,所以服務端必須對前端參數做一些驗證,包括但不限于特殊字符轉義、標簽、屬性白名單過濾等。一旦出現安全問題一般都是挺嚴重的,不管是敏感數據被竊取或者用戶資金被盜,損失往往無法挽回。我們平時開發中需要保持安全意識,保持代碼的可靠性和安全性。
作者:陳吉
轉發鏈接:https://mp.weixin.qq.com/s/HweEFh78WXLawyQr_Vsl5g