解密 React element 的 $$typeof 属性

时间:2021-1-8 作者:admin

译:Why Do React Elements Have a $typeof Property?

December 10, 2018

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

译自 Dan Abramov 的博客,原文链接

编写 JSX 的时候,你或许觉得你只是在写:

<marquee bgcolor="#ffa7c4">h1</marquee>

其实,你是在调用一个方法:

React.createElement(
  /* type */ 'marquee',
  /* props */ { bgcolor: '#ffa7c4' },
  /* children */ 'hi'
)

这个方法会返回给你一个对象。我们则称这个对象为 React _元素_(React _element_). 它告诉 React 接下来要如何进行渲染。你的组件便会返回一个基于这个对象的树形结构。

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi'
  },
  key: null,
  ref: null,
  $typeof: Symbol.for('react.element'), // 🧐 Who dis
}

如果你使用过 React,一定对 type, props, keyref 非常熟悉。 但什么是$typeof呢?为什么它是一个Symbol()类型的值呢?

其实这个问题属于“你不一定需要知道的 React”系列,但它能提升你在 coding 过程中的幸福感。这篇文章也包含了一些你可能想知道安全性小贴士。或许有一天你会编写自己的 UI 库,这些知识能使你得心应手。这便是我诚心所念。


客户端 UI 库普及并且拥有一些基本的保护机制之前,我们通常会先构造一个 HTML 结构,然后将它插入到 DOM 中去:

const messageEl = document.getElementById('message');
messageEl.innerHTML = '<p>' + message.text + '</p>';

这样做似乎没有问题,除非你的message.text是类似<img src onerror="stealyearPassword()">这样的东西。你并不希望陌生人编写的东西逐字逐行的打印在你的 HTML 中。

(有趣之处:如果你只进行客户端渲染,那么这种形式的<script>标签中的 JavaScript 代码不会生效。但是别让这个事实麻痹了你的安全意识。)

你可以使用可靠的 API 来防御这样的注入攻击,如document.createTextNode()或者textContent等方式,它们只会生成纯文本节点。 你也可以对用户输入进行“escape”处理,先发制人地将<, >或者其他用户提供的文本替换成安全的字符。

但这看起来仍然不够,要记住在每一次将用户输入进行输出的时候进行escape处理是一件相当麻烦的事,而某一次的疏忽就可能导致出错,这引起的开销是非常高昂的。这就是为什么现代 lib 会默认的对文本内容进行escape,譬如 React。

<p>
  {message.text}
</p>

如果message.text是一个包含<img>等标签的恶意字符串,那么这些标签不会被渲染成真正的 HTML 节点。React 会先将这些不可靠的内容回避掉,然后将处理后的数据插入到 DOM 中。所以你只会看到<img>或是其它标签的文本标记,而不是对应的 HTML 节点。

要在 React element 中渲染一个任意的 HTML 串,你需要使用dangerouslySetInnerHTML={{ __html: message.text }}事实上这个特性相当的笨拙。其意义在于提高相关功能在 code review 或代码审查时的可见性。


这代表 React 可以完全抵御注入攻击吗?不。HTML 和 DOM 提供了大量可被攻击的接口,React 或者其他的 UI 库难以对它们一一处理。这里面大多数的攻击方式都面向属性。例如,如果你渲染一个<a href={user.website}>这样的标签,就得谨防遇到'javascript:stealYourPassword()'这样的用户页面。再如,像<div {...userData}>这样展开用户输入的实践方式很少见,但同样是不可靠的。

随着技术的不断演进 React 能够提供更多的保护机制,但这些问题其实有相当一部分来自于服务端,并且应当在那里解决。

当然了,对文本内容进行 escape 处理依然是防御潜在入侵的第一“教条”。对这样的代码放心不是一件很 nice 的事吗?~

// 会自动回避不安全的字符串
<p>
  {message.text}
</p>

不过,也不是总能如意。这就是$typeof出现的原因。


React 元素的本质上是一个纯粹的对象:

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi'
  },
  key: null,
  ref: null,
  $typeof: Symbol.for('react.element')
}

React 确切提供了通过以上的纯对象方式创建元素的方法。通常来说,你能用React.createElement()来创建它们,不过这是可选的方式。当然了,你应该并不想像这样来编写代码——但这对优化编译器、workers(译者注:指 webworker)间的 UI 对象传递(译者注:你没办法在 webworker 中使用 JSX)或是从 React 代码包中解耦 JSX 来说是有好处的。

然而,如果你的服务器存在一个漏洞,使得用户可以存储任意的JSON对象,但客户端却需要一个 string 类型的值,这就是个麻烦了:

// Server could have a hole that lets user store JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* put your exploit here */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// Dangerous in React 0.13
<p>
  {message.text}
</p>

在这个 XSS 注入攻击的案例中,React 0.13 会显得非常弱势(译者注:即误将对象渲染成元素)。再次声明,这个攻击主要依赖于一个服务端漏洞。而 React 依然能为避免这个问题做一些更棒的处理。从 0.14 开始,React 做到了。

React 0.14 通过给每个 React 元素标记一个 Symbol 类型的属性来解决这个问题:

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi'
  },
  key: null,
  ref: null,
  $typeof: Symbol.for('react.element')
}

其利用 JSON 属性值不能为 Symbol 这个性质来工作。即便服务端有一个安全漏洞会返回 JSON 而不是普通文本,JSON 中的属性值也不可能为 Symbol.for(‘react.element’)。React 会检查element.$typeof,并且回避掉丢失了这个属性或者其属性检查不通过的元素。

Symbol.for()好处在于Symbols是有很高的全局性的,因此在一些特殊环境如 iframe 或 webworker 上下文中也可以使用 Symbol.for() 查询到对应的规则。因此,即便在相当特殊的情景下,应用中不同部分也可以顺利的传递可靠的元素。同样的,即便同一个页面中有多份 React 实例的拷贝,它们也能在$typeof属性之上“达成共识”。


那么如果浏览器不支持 Symbol 类型怎么办呢?

哎,它们确实得不到这个特别的保护机制。但 React 为了保持一致性依然会在元素中定义$typeof属性,只不过它会被设置成一个 number 类型的值——0xeac7.

为什么是这个特别的数字呢?因为0xeac7看起来有点像“React”。


声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。