你覺得你在寫JSX:
<marquee bgcolor="#ffa7c4">hi</marquee>
複製程式碼
其實,你在呼叫一個方法:
React.createElement(
/* type */ 'marquee',
/* props */ { bgcolor: '#ffa7c4' },
/* children */ 'hi'
)
複製程式碼
之後方法會返回一個物件給你,我們稱此物件為React的 元素(element),它告訴React下一個要渲染什麼。你的元件(component)返回一個它們組成的樹(tree)。
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi',
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'), // ? Who dis
}
複製程式碼
如果你用過React,對type
、 props
、 key
、 和 ref
應該熟悉。 但 $$typeof
是什麼?為什麼用 Symbol()
作為它的值?
這又是一個與你學習使用React不 相關 的點,但瞭解後你會覺得舒坦。這篇文章裡也提到了些關於安全的提示,你可能會感興趣。也許有一天你會有自己的UI庫,這些都會派上用場的,我真的希望如此。
在客戶端UI庫變得普遍且具有基本保護作用之前,應用程式程式碼通常是先構建 HTML,然後把它插入DOM中:
const messageEl = document.getElementById('message');
messageEl.innerHTML = '<p>' + message.text + '</p>';
複製程式碼
這樣看起來沒什麼問題,但當你 message.text
的值類似 '<img src onerror="stealYourPassword()">'
時,你不會希望別人寫的內容在你應用的HTML中逐字顯示的。
(有趣的是:如果你只是在前端渲染,這裡為 <script>
標籤,JavaScript程式碼不會被執行。但不要因此讓你陷入已經安全的錯覺。)
為什麼防止此類攻擊,你可以用只處理文字的 document.createTextNode()
或者 textContent
等安全的API。你也可以事先將使用者輸入的內容,用轉義符把潛在危險字元(<
、>
等)替換掉。
儘管如此,這個問題的成本代價很高,且很難做到使用者每次輸入都記得轉換一次。因此像React等新庫會預設進行文字轉義:
<p>
{message.text}
</p>
複製程式碼
如果 message.text
是一個帶有 <img>
或其他標籤的惡意字串,它不會被當成真的 <img>
標籤處理,React會先進行轉義 然後 插入DOM裡。所以<img>
標籤會以文字的形式展現出來。
要在React元素中渲染任意HTML,你不得不寫dangerouslySetInnerHTML={{ __html: message.text }}
。其實這種愚蠢的寫法是一個功能,在code reviews和程式碼庫稽核時,你可以非常清晰的定位到程式碼。
這意味著React完全不懼注入攻擊了嗎?不,HTML和DOM暴露了大量攻擊點,對React或者其他UI庫來說,要減輕傷害太難或進展緩慢。大部分存在的攻擊方向涉及到屬性,例如,如果你渲染<a href={user.website}
,要提防使用者的網址是 'javascript: stealYourPassword()'
。 像<div {...userData}>
寫法幾乎不受使用者輸入影響,但也有危險。
React可以逐步提供更多保護,但在很多情況下,威脅是伺服器產生的,這不管怎樣都應該要避免。
不過,轉義文字這第一道防線可以攔下許多潛在攻擊,知道這樣的程式碼是安全的就夠了嗎?
// Escaped automatically
<p>
{message.text}
</p>
複製程式碼
好吧,也不總是有效的。這就是 $$typeof
的用武之地了。
React元素(elements)是設計好的 plain object:
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi',
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'),
}
複製程式碼
雖然通常用 React.createElement()
建立它,但這不是必須的。有一些React用例來證實像上面這樣的 plain object元素是有效的。當然,你不會 想 這樣寫的,但這可以用來優化編譯器,在 workers 之間傳遞UI元素,或者將JSX從React包解耦出來。
但是,如果你的伺服器有允許使用者儲存任意JSON物件的漏洞,而前端需要一個字串,這可能會發生一個問題:
// 服務端允許使用者儲存JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 把你想的擱著 */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// Dangerous in React 0.13
<p>
{message.text}
</p>
複製程式碼
在這個例子中,React 0.13很容易受到XSS攻擊。再次宣告,這個攻擊是服務端存在漏洞導致的。不過,React會為了大家的安全做更多工作。從React 0.14開始,它做到了。
React 0.14修復手段是用Symbol標記每個React元素(element):
{
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 通用於 iframes 和 workers 等環境中。因此無論在多奇怪的條件下,這方案也不會影響到應用不同部分傳遞可信的元素。同樣,即使頁面上有很多個React副本,它們也 “接受” 有效的$$typeof
值。
如果瀏覽器不支援 Symbols 怎麼辦?
唉,那這種保護方案就無效了。React仍然會加上 $$typeof
欄位以保證一致性,但只是設定一個數字而已—— 0xeac7
。
為什麼是這個數字?因為 0xeac7
看起來有點像 “React”。
翻譯原文Why Do React Elements Have a $$typeof Property?(2018-12-03)