文章标签: react.js javascript 前端
版权
举个例子:例如我们的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)。
想查看具体的攻击流程,可以查看这篇博客。