React用户安全的第一道防线是什么?React是怎么预防XSS攻击的?
当你在写 JSX 时,其实你在调用createElement
方法。
React.createElement( /* type */ 'marquee', /* props */ { bgcolor: '#ffa7c4' }, /* children */ 'hi' ) 复制代码
createElement
会返回一个对象,我们称此对象为React的 元素(element),它告诉 React 下一个要渲染什么。你的组件(component)返回一个它们组成的树(tree)。
{ type: 'marquee', props: { //... }, key: null, ref: null, $$typeof: Symbol.for('react.element'), } 复制代码
在客户端 UI 库变得普遍且具有基本保护作用之前,应用程序代码通常是先构建 HTML,然后把它插入 DOM 中:
const messageEl = document.getElementById('message'); messageEl.innerHTML = '<p>' + message.text + '</p>'; 复制代码
这样看起来没什么问题,但当你 message.text
的值类似 '<img src onerror="stealYourPassword()">'
时, 你不会希望别人写的内容在你应用的 HTML 中逐字显示的。
为什么防止此类攻击,你可以用只处理文本的 document.createTextNode()
或者 textContent
等安全的 API。你也可以事先将用户输入的内容,用转义符把潜在危险字符( <
、 >
等)替换掉。
尽管如此,这个问题的成本代价很高,且很难做到用户每次输入都记得转换一次。 因此像React等新库会默认进行文本转义:
如果 message.text
是一个带有 <img>
或其他标签的恶意字符串,它不会被当成真的 <img>
标签处理,React 会先进行转义然后插入 DOM 里。所以 <img>
标签会以文本的形式展现出来。
在 React 中如果元素要渲染 HTML,那么需要使用 dangerouslySetInnerHTML={{ __html: message.text }}
这意味着React完全不惧注入攻击了吗?不,HTML 和 DOM 暴露了大量攻击点,对 React 或者其他 UI 库来说,要减轻伤害太难或进展缓慢。大部分存在的攻击方向涉及到属性,例如,如果你渲染 <a href={user.website}
,要提防用户的网址是 'javascript: stealYourPassword()'
。 像 <div {...userData}>
写法几乎不受用户输入影响,但也有危险。
不过,转义文本这第一道防线可以拦下许多潜在攻击,知道这样的代码是安全的就够了吗?不一定,所以我们需要$$typeof
如果你用过 React,对 type
、 props
、 key
、 和 ref
应该熟悉。 但你不一定知道 $$typeof
?
如果你的服务器有允许用户存储任意 JSON 对象的漏洞,而前端需要一个字符串,这可能会发生一个问题:
// 服务端允许用户存储 JSON let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* 把你想的放在这里 */' }, }, // ... }; let message = { text: expectedTextButGotJSON }; // React 0.13 中有风险 <p> {message.text} </p> 复制代码
在这个例子中,React 0.13 很容易受到 XSS 攻击。虽然 这个攻击是服务端存在漏洞导致的。不过,从 React 0.14 开始,这个问题修复了。
React 0.14 修复手段是在虚拟DOM中添加 $$typeof
,使用 Symbol
标记每个 React 元素(element):
Symbol
类型是非常重要的,因为JSON不支持 Symbol
类型。 所以即使服务器存在用JSON作为文本返回安全漏洞,JSON 里也不包含 Symbol.for('react.element')
。React 会检测 element.$$typeof
,如果元素丢失或者无效,会拒绝处理该元素。
特意用 Symbol.for()
的好处是 Symbols 通用于 iframes 和 workers 等环境中。因此无论在多奇怪的条件下,这方案也不会影响到应用不同部分传递可信的元素。同样,即使页面上有很多个 React 副本,它们也 「接受」 有效的 $$typeof
值。
为什么是这个数字?因为 0xeac7
看起来有点像 「React」。