本文会先回顾xss攻击的概念和类型,再介绍react中做了哪些事情来实现xss安全性防御。
XSS 攻击
Cross-Site Scripting (跨站脚步攻击)简称XSS, 是一种代码注入攻击。XSS攻击通常指利用网页漏洞,攻击者通过技巧注入xss代码到网页,因为浏览器无法分辨哪些是可信的,导致XSS脚步被执行。XSS脚本通常能够窃取用户数据并发送到攻击者的网站,或者冒充用户,调用目标接口并执行攻击者指定操作。
XSS攻击类型
反射型
- XSS脚本来自当前HTTP请求
- 当服务器在HTTP请求中接收数据并将该数据拼接在HTML中返回时
1 2 3 4 5 6 7 8 9
| // 某网站具有搜索功能,该功能通过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 脚本来自服务器数据库中
攻击者将恶意代码提交到目标网站的数据库中,普通用户访问网站时服务器将恶意代码返回,浏览器默认执行
// 某个评论页,能查看用户评论
// 攻击者将恶意代码当做评论提交,服务器没对数据进行转义等处理
// 评论输入:
<textarea>
<img src="empty.png" onerror="alert("xss")" />
</textarea>
// 则攻击者提供的脚本将在所有访问该评论页的用户浏览器执行
DOM型XSS
该漏洞存在于客户端代码,与服务器无关
- 类似反射型,区别在于DOM型XSS并不会和后台进行交互,前端直接将URL中的数据不做处理并动态插入到HTML中,是纯粹的前端安全问题,要做防御也只能在客户端进行防御。
React如何防止XSS攻击
无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。react官方提到 React DOM
在渲染所有输入内容之前,默认会进行转义。它可以确保在应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换为字符串,因此恶意代码无法成功注入,从而有效的防止XSS攻击。
具体如下:
自动转义
React 在渲染HTML内容和渲染DOM属性时会将" ' & < >
这几个字符进行转义,转义部分源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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前都被转成字符串,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| var p = document.getElementById("p"); // 一段恶意代码 const str = `<img src="empty.png" onerror="alert('xss')" />`; let _str = ""; for (let index = 0; index < str.length; index++) { let escape = ""; 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: escape = str[index]; // continue; } console.log(escape); _str += escape; } console.log(_str); // <img src="empty.png" onerror="alert('xss')" /> p.innerHTML = `您的搜索是:${_str}`;
|
JSX语法
JSX实际上是一种语法糖,Babel会把JSX编译成React.createElement()
函数调用,最终返回一个ReactElement
,以下为这几个步骤的对应代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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" } ... }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const symbolFor = Symbol.for; REACT_ELEMENT_TYPE = symbolFor("react.element"); const ReactElement = function(type, key, ref, self, source, owner, props){ const element ={ $$typeof: REACT_ELEMENT_TYPE, type:type, key: key, ref: ref, props: props, _owner: owner, } ... return element; }
|
$$typeof
标记对象是一个ReactElement
属性,在渲染时会通过此属性校验,校验不通过会抛出错误。React利用这个属性来防止通过构造特殊的Children
来进行XSS攻击,原因是$$typeof
是个Symbol类型,进行JSON转换后Symbol值会丢失,无法在前后端进行传输。如果用户提交了特殊的Children,也无法进行渲染,利用此特性,可以防止存储型XSS攻击。
在React中可引起的漏洞的一些写法
使用dangerouslySetInnerHTML
dangerouslySetInnerHTML
是React为浏览器DOM提供的innerHTML
替换方案,通常来讲,使用代码直接设置HTML存在的风险,因为很容易使用户暴露在XSS攻击下,因为当使用dangerouslySetInnerHTML
时, React将不会对输入进行任何处理并直接渲染到HTML中,如果攻击者在dangerouslySetInerHTML
传入了恶意代码,那么浏览器将会运行恶意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // dangerouslySetInerHTML 源码 function getNonChildrenInnerMarkup(props){ const innerHTML = props.dangerouslySetInnerHTML; 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
, 如果不得不使用,必须对输入进行相关的验证,例如对特殊输入进行过滤、转义等处理。前端这边可以使用xss
白名单过滤,通过白名单控制允许HTML标签及各标签属性。
通过用户提供的对象来创建React组件
1 2 3 4 5 6 7
| // 用户输入 const userProvidePropString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\")' src='empty.png' /> "}}`; // 经过JSON转换 const userProvideProps = JSON.parse(userProvidePropsString); render(){ return <div {...userProvideProps} /> }
|
中断代码将用户提供的数据进行json转换后直接当做div的属性,当用户构造了一个类似列子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户输入作为属性。
使用用户输入的值来渲染a标签的href
属性,或类似的img 标签的src属性等
1 2
| const userWebsite = "javascript:alert('xss');"; <a href={userWebsite}></a>
|
如果没有对该URL进行过滤以防止通过JavaScript:
或data:
来执行JavaScript
,则攻击者可以构造XSS攻击,此处会有潜在的安全问题。用户提供的URL需要前端或服务端在入库之前进行校验并过滤。
服务端如何防止XSS攻击
服务端作为最后一道防线,也需要做一些措施以防止XSS攻击的,一般涉及以下几方面:
- 在收到用户输入时,需要对输入尽可能的严格过滤,过滤或移除特殊的HTML标签、js事件的关键字等
- 在输出时对数据进行转义,根据输出语境进行对应的转义
- 对关键Cookie设置
http-only
属性,js脚本就不能访问到http-only
的Cookie
- 利用CSP来抵御或削弱XSS攻击,一个CSP兼容的浏览器将会仅执行从白名单获取到的脚本文件,忽略所有的其他脚本(包括内联脚本和HTML的事件处理属性)
CSP
1
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
|