​ 本文会先回顾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 = "&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前都被转成字符串,如下

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 = "&quot;";
break;
case 38: // &
escape = "&amp;";
break;
case 39: // '
escape = "&#x27;";
break;
case 60: // <
escape = "&lt;";
break;
case 62: // >
escape = "&gt;";
break;
default:
escape = str[index];
// continue;
}
console.log(escape);
_str += escape;
}
console.log(_str); // &lt;img src=&quot;empty.png&quot; onerror=&quot;alert(&#x27;xss&#x27;)&quot; /&gt;
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 ={
// 这个tag唯一标识了此为ReactElement
$$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';">