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)。

想查看具體的攻擊流程,可以查看這篇博客

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