React Elements为什么要有一个$typeof属性
假如我们的jsx长这个样子:
<Button type="primary">点击</Button>
实际上,在经过babel后,它会变成下面这段代码:
React.createElement(
/* type */ 'Button',
/* props */ { type: 'primary' },
/* children */ '点击'
)
之后,这个函数执行结果会返回一个对象,这个对象我们称为React Element
。它是一个用来描述我们将要渲染的页面结构的一个不可变对象。想了解更多与React Component
,Elements
和Inastances
的可以点击这里。
// React Element
{
type: 'Button',
props: {
type: 'primary',
children: '点击',
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'), // 为什么有这个东西
}
对于React开发者来说,上面这些属性大部分都是比较常见的。可是为什么混进了一个奇怪的$$typeof
??它是干嘛的呢?它的值为什么是一个Symbol
呢?
这个属性的引入,其实要从一个安全漏洞说起。
假如我们要显示一个变量,如果你使用纯js来写的话,可能是这样:
const messageEl = document.getElementById('message');
messageEl.innerHTML = `<div>${message}</div>`;
这一段代码,对于熟悉或者了解过XSS攻击的人来说,一看就知道会有问题,存在着XSS攻击。如果message
是用户可以控制的变量(比如说是用户输入的评论)的话,那么用户就可以进行攻击了。比如用户可以构造下面的代码来进行攻击:
message = '<img onerror="alert(2)" src="" />';
这样页面一加载到这段代码,就会弹出一个alert框。
如果我们明确知道,我们只想单纯的渲染文本,不想把它当成html来渲染的话,那么我们可以通过textContent来避免这个问题。
const messageEl = document.getElementById('message');
messageEl.textContent = `<div>${message}</div>`;
而对于React而言的话,想要实现相同的效果,只需要:
<div>{message}</div>
即使message里面含有img
、script
类似的标签,它们最终也不会以实际上的标签显示。React会对渲染的内容进行转译,比如说上面的攻击代码会被转译为:
message = '<img onerror="alert(2)" src=""/>';
// 转译为
message = '<img onerror="alert(2)" src=""/>'
因此,这样就可以避免大部分场景下的XSS攻击了。
当然,React也提供了另一种方式来将用户输入的内容当成html来渲染:
<div dangerouslySetInnerHTML={{ __html: message }}></div>
前面说了这么多,那么跟$$typeof
又有什么关系呢?别急,重点来了。
对于下面这种写法,我们一般都知道,message可以传基本类型、自定义组件和jsx片段。
<div>{message}</div>
可是,其实我们还可以直接传React Element。比如,我们可以直接这样写
class App extends React.Component {
render() {
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
$$typeof: Symbol.for("react.element")
};
return <>{message}</>;
}
}
这样在运行的时候,就会弹出一个alert框了。查看demo。那么,这样会有什么风险呢?
考虑一个场景,比如一个博客网站的评论信息message
是由用户提供的,并且支持传入JSON。那么如果用户直接将上文的message发送给后台保存。之后,通过下面这种方式展示的话,用户就可以进行XSS攻击了。
<div>{message}</div>
假设如果没有$$typeof属性的话,这种攻击确实可行。因为其他的属性都是可序列化的。
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
};
JSON.stringify(message);
事实上,React 0.13当时就存在着这个漏洞。之后,React 0.14就修复了这个问题,修复方式就是通过引入$$typeof属性,并且用Symbol来作为它的值。
// 引入 $$typeof
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `<h1>Arbitrary HTML</h1>
<img onerror="alert(2)" src="" />
<a href='http://danlec.com'>link</a>`
}
},
key: null,
ref: null,
$$typeof: Symbol.for("react.element")
};
JSON.stringify(message); // Symbol无法被序列化
这是一个有效的方法,因为JSON是不支持Symbol
类型的。所以,即使用户提交了如上的message
信息,到最后服务端也不会保存$$typeof属性。而在渲染的时候,React 会检测是否有$$typeof
属性。如果没有这个属性,则拒绝处理该元素。
那么如果浏览器不支持Symbol
怎么办?
是的,那这种保护方案就没用了。React 依然会加上$$typeof字段,并且将其值设置为0xeac7
。(为什么是这个数字呢,因为这个数字看起来有点像React
)。
想查看具体的攻击流程,可以查看这篇博客。