Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 是如何防止 XSS 攻击的,论$$typeof 的作用 #17

Open
lizuncong opened this issue Jul 21, 2022 · 0 comments
Open

React 是如何防止 XSS 攻击的,论$$typeof 的作用 #17

lizuncong opened this issue Jul 21, 2022 · 0 comments

Comments

@lizuncong
Copy link
Owner

JSX

先来简单复习一下 JSX 的基础知识。JSX 是React.createElement的语法糖

<div id="container">hello</div>

经过 babel 编译后:

React.createElement(
  "div" /* type */,
  { id: "container" } /* props */,
  "hello" /* children */
);

React.createElement最终返回的结果就是一个对象,如下:

{
  type: 'div',
  props: {
    id: 'container',
    children: 'hello',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

这就是一个 React element 对象。

我们甚至可以在代码中直接写 React element 对象,React 照样能正常渲染我们的内容:

render() {
  return (
    <div>
      {{
        $$typeof: Symbol.for('react.element'),
        props: {
          dangerouslySetInnerHTML: {
            __html: '<img src="x" onerror="alert(1)">'
          },
        },
        ref: null,
        type: "div",
      }}
    </div>
  );
}

可以复制这段代码本地运行一下,可以发现浏览器弹出一个弹窗,并且img已经插入了 dom 中。

这里,$$typeof 的作用是啥?为什么使用 Symbol() 作为值?

在了解之前,我们先来简单看下 XSS 攻击

XSS 攻击

我们经常需要构造 HTML 字符串并插入到 DOM 中,比如:

const messageEl = document.getElementById("message");
var message = "hello world";
messageEl.innerHTML = "<p>" + message + "</p>";

页面正常显示。但是如果我们插入一些恶意代码,比如:

const messageEl = document.getElementById("message");
var message = '<img src onerror="alert(1)">';
messageEl.innerHTML = "<p>" + message + "</p>";

此时页面就会弹出一个弹窗,弹窗内容显示为 1

因此,直接使用 innerHTML 插入文本内容,存在 XSS 攻击的风险

防止 XSS 攻击的方法

为了解决类似的 XSS 攻击方法,我们可以使用一些安全的 API 添加文本内容,比如:

  • 使用 document.createTextNode('hello world') 插入文本节点。
  • 或者使用 textContent 而不是 innerHTML 设置文本内容。
  • 对于一些特殊字符,比如 <>,我们可以进行转义,将其转换为 &#60; 以及 &#62;
  • 对于富文本内容,我们可以设置黑名单,过滤一些属性,比如 onerror 等。

React 对于文本节点的处理

React 使用 createTextNode 或者 textContent 设置文本内容。对于下面的代码

render() {
  const { count } = this.state
  return (
    <div onClick={() => this.setState({ count: count + 1})}>
      {count}
    </div>
  );
}

React 在渲染过程中会调用setTextContent方法为div节点设置内容,其中,第一次渲染时,直接设置div节点的textContent,第二次或者第二次以后的更新渲染,由于第一次设置了textContent,因此divfirstChild值存在,是个文本节点。此时直接更新这个文本节点的nodeValue即可

var setTextContent = function (node, text) {
  if (text) {
    var firstChild = node.firstChild;
    // 如果当前node节点已经设置过textContent,则firstChild不为空,是个文本节点TEXT_NODE
    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  // 第一次渲染,直接设置textContent
  node.textContent = text;
};

综上,对于普通的文本节点来说,由于 React 是采用 textContent 或者 createTextNode 的方式添加的,因此是不会存在 XSS 攻击的,即使上面示例中,count 的值为 '<img src="x" onerror="alert(1)">'也不会有被攻击的风险

dangerouslySetInnerHTML 处理富文本节点

有时候我们确实需要显示富文本的内容,React 提供了dangerouslySetInnerHTML方便我们显式的插入富文本内容

render() {
  return (
    <div
      id="dangerous"
      dangerouslySetInnerHTML={{ __html: '<img src="x" onerror="alert(1)">' }}
    >
    </div>
  );
}

React 在为 DOM 更新属性时,会判断属性的key是不是dangerouslySetInnerHTML,如果是的话,调用setInnerHTML 方法直接给 dom 的innerHTML属性设置文本内容

function setInitialDOMProperties(
  tag,
  domElement,
  rootContainerElement,
  nextProps
) {
  for (var propKey in nextProps) {
    if (propKey === "dangerouslySetInnerHTML") {
      var nextHtml = nextProp ? nextProp.__html : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    }
  }
}
var setInnerHTML = function (node, html) {
  node.innerHTML = html;
};

可以看出,React 在处理富文本时,也仅仅是简单的设置 DOM 的innerHTML属性来实现的。

对于富文本潜在的安全风险,交由开发者自行把控。

$$typeof 的作用

render() {
  const { text } = this.state
  return (
    <div>
      {text}
    </div>
  );
}

假设这个text是从后端返回来的,同时后端允许用户存储 JSON 对象,如果用户传入下面这样的一个类似 React element 的对象:

{
  type: "div",
  props: {
    dangerouslySetInnerHTML: {
      __html: '<img src="x" onerror="alert(1)">'
    },
  },
  ref: null
}

别忘了前面说过,我们在 JSX 中直接插入 React element 对象也是能够正常渲染的。

在这种情况下,在React0.13版本时,这是一个潜在的XSS攻击,这个漏洞源于服务端。如果攻击者恶意伪造一个类似 React element 对象的数据返回给前端,React 就会执行恶意代码。但是 React 可以采取措施预防这种攻击。

React0.14版本开始,React 为每个 element 都添加了一个Symbol标志:

{
  $$typeof: Symbol.for('react.element'),
  props: {
    id: 'container'
  },
  ref: null,
  type: "div",
}

这个行得通,是因为 JSON 不支持Symbol。因此即使是服务端有风险漏洞并且返回一个 JSON,这个 JSON 也不会包含Symbol.for('react.element').,在 Reconcile 阶段,React 会检查element.$$typeof标志是否合法。不合法的话直接报错,React 不能接受对象作为 children

专门使用 Symbol.for() 的好处是, Symbols 在 iframe 和 worker 等环境之间是全局的。因此,即使在更奇特的条件下,Symbols 也能在不同的应用程序之间传递受信任的元素。同样,即使页面上有多个 React 副本,它们仍然可以“同意”有效的 $$typeof 值

如果浏览器不支持Symbols,React 使用0xeac7代替

{
  $$typeof: '0xeac7',
}

参考链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant