浅谈 React 中的 XSS 攻击( 二 )

这段代码是 React 在渲染到浏览器前进行的转义 , 可以看到对浏览器有特殊含义的字符都被转义了 , 恶意代码在渲染到 HTML 全都被转成了字符串 , 如下:
// 一段恶意代码<img src=https://www.isolves.com/it/aq/sj/2020-09-24/"empty.png" onerror ="alert('xss')"> // 转义后输出到 html 中浅谈 React 中的 XSS 攻击 这样就有效的防止了 XSS 攻击 。
JSX 语法JSX 实际上是一种语法糖 , Babel 会把 JSX 编译成 React.createElement() 的函数调用 , 最终返回一个 ReactElement , 以下为这几个步骤对应的代码:
// JSXconst element = (  <h1 className="greeting">    Hello, world!  </h1>);// 通过 babel 编译后的代码const element = React.createElement(  'h1',  {className: 'greeting'},  'Hello, world!');// React.createElement() 方法返回的 ReactElementconst element = {  $$typeof: Symbol('react.element'),  type: 'h1',  key: null,  props: {    children: 'Hello, world!',      className: 'greeting'     }  ...}我们可以看到 , 最终渲染的内容是在 Children 属性中 , 那了解了 JSX 的原理后 , 我们来试试能否通过构造特殊的 Children 进行 XSS 注入 , 来看下面一段代码:
const storedData = `{  "ref":null,  "type":"body",  "props":{  "dangerouslySetInnerHTML":{  "__html":"<img src=https://www.isolves.com/it/aq/sj/2020-09-24/"empty.png" onerror ="alert('xss')"/>" } }}`;// 转成 JSONconst parsedData = JSON.parse(storedData);// 将数据渲染到页面render () { return {parsedData} ; }这段代码中 ,  运行后会报以下错误 , 提示不是有效的 ReactChild
Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.那究竟是哪里出问题了?我们看一下 ReactElement 的源码:
const symbolFor = Symbol.for;REACT_ELEMENT_TYPE = symbolFor('react.element');const ReactElement = function(type, key, ref, self, source, owner, props) {  const element = {    // 这个 tag 唯一标识了此为 ReactElement    $$typeof: REACT_ELEMENT_TYPE,    // 元素的内置属性    type: type,    key: key,    ref: ref,    props: props,    // 记录创建此元素的组件    _owner: owner,  };  ...  return element;}注意到其中有个属性是 $$typeof , 它是用来标记此对象是一个 ReactElement , React 在进行渲染前会通过此属性进行校验 , 校验不通过将会抛出上面的错误 。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击 , 原因是 $$typeof 是个 Symbol 类型 , 进行 JSON 转换后会 Symbol 值会丢失 , 无法在前后端进行传输 。如果用户提交了特殊的 Children , 也无法进行渲染 , 利用此特性 , 可以防止存储型的 XSS 攻击 。
在 React 中可引起漏洞的一些写法使用 dangerouslySetInnerHTMLdangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案 。通常来讲 , 使用代码直接设置 HTML 存在风险 , 因为很容易使用户暴露在 XSS 攻击下 , 因为当使用 dangerouslySetInnerHTML 时 , React 将不会对输入进行任何处理并直接渲染到 HTML 中 , 如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码 , 那么浏览器将会运行恶意代码 。看下源码:
function getNonChildrenInnerMarkup(props) {  const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性 , 会不经转义就渲染__html的内容  if (innerHTML != null) {    if (innerHTML.__html != null) {      return innerHTML.__html;    }  } else {    const content = props.children;    if (typeof content === 'string' || typeof content === 'number') {      return escapeTextForBrowser(content);    }  }  return null;}


推荐阅读