React Elements为什么要有一个$typeof属性

React Elements为什么要有一个$typeof属性

假如我们的jsx长这个样子:

<Button type="primary">点击</Button>

实际上,在经过babel后,它会变成下面这段代码:

React.createElement(
  /* type */ 'Button',
  /* props */ { type: 'primary' },
  /* children */ '点击'
)

之后,这个函数执行结果会返回一个对象,这个对象我们称为React Element。它是一个用来描述我们将要渲染的页面结构的一个不可变对象。想了解更多与React Component,ElementsInastances的可以点击这里

// 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里面含有imgscript类似的标签,它们最终也不会以实际上的标签显示。React会对渲染的内容进行转译,比如说上面的攻击代码会被转译为:

message = '<img onerror="alert(2)" src=""/>';
// 转译为
message = '&lt;img onerror="alert(2)" src=""/&gt;'

因此,这样就可以避免大部分场景下的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)。

想查看具体的攻击流程,可以查看这篇博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章