背景
在 React 项目中常会遇到渲染 HTML 内容的情况。可以利用 react 的 dangerouslySetInnerHTML 属性,完成基础开发。
示例:
function createMarkup() {
return {__html: 'First · Second'};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={createMarkup()} />;
}
不足
就作者目前查阅的资料和实践结果,上文提到的基础方案有一些不足。
以非虚拟 DOM 的方式渲染节点
React 对虚拟 DOM 设计了优化的算法(主要依赖 data-reactid
),放弃走虚拟 DOM 的渲染等同于放弃这些优化。
而 dangerouslySetInnerHTML
的渲染方式类似于原生 JS 的 HTML 渲染,显然放弃了节点优化:
function createMarkup() {
return {__html: '<div style="color: red">I m cool<p></p></div>'};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={createMarkup()} />;
}
实验下来,只有 dangerouslySetInnerHTML
所在的元素上带有 data-reactid
,而子元素都没有。
可以从 React 源码中证实:
// ReactDOMComponent.js 部分源码:
// 为方便阅读,只保留了 _createContentMarkup 函数的相关代码
/**
* Creates markup for the content between the tags.
*
* @private
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {object} props
* @param {object} context
* @return {string} Content markup.
*/
_createContentMarkup: function (transaction, props, context) {
var ret = '';
var innerHTML = props.dangerouslySetInnerHTML; // 拿到 dangerouslySetInnerHTML 内容
ret = innerHTML.__html;
return ret;
}
// 为方便阅读,只保留了 ReactDOMComponent.Mixin.mountComponent 函数的相关代码
ReactDOMComponent.Mixin = {
/**
* Generates root tag markup then recurses. This method has side effects and
* is not idempotent.
*
* @internal
* @param {string} rootID The root DOM ID for this node.
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {object} context
* @return {string} The computed markup.
*/
mountComponent: function (rootID, transaction, context) {
this._rootNodeID = rootID;
var props = this._currentElement.props;
var mountImage;
//...
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
// 使用 _createContentMarkup
var tagContent = this._createContentMarkup(transaction, props, context);
// 返回最终 dom 字符串只是把 _createContentMarkup 生成的 HTML 包裹一下
mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
return mountImage;
}
XSS 攻击
关于 XSS 的话题有点大,作者仅以自己的实验说明:
function createMarkup() {
return {__html: `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={createMarkup()} />;
}
在基础方案下,input 元素完美渲染,点击正常,如果是恶意数据源,很容易造成严重后果。因此过滤必不可少。
解决思路
1.弃用 dangerouslySetInnerHTML,把文本 HTML 内容转化为 React-DOM 对象。
从 React
0.x 过来的小伙伴应该还没忘记没有 JSX 的时代,手写 React DOM 对象的开发方式。就算到了如今 JSX 也是先转换成 React DOM 对象再进行后面的渲染。
把 HTML 翻译成对象数组目前已有成熟的方案,htmlparse2 是个不错的选择。
不过 htmlparse2
生成的对象跟 React 特有的 DOM 对象还有一定距离,需要做进一步的转换,开源库 react-html-parser
这里做了不错的示范。
//使用 react-html-parser 后:
import ReactHtmlParser from 'react-html-parser';
let html = `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`;
function MyComponent() {
return <div>{ ReactHtmlParser(html) }<div/>;
}
2.过滤高危元素
防止 XSS 攻击的主要手段之一就是过滤危险标签,例如 input
这种类型的元素则是重点「嫌疑人」。基于上面提到的对象转化,做到过滤并不难。安全等级和体验的平衡,取决于我们对转化后的对象的细致过滤。比如发现 tag 类型是 input 时一棍子打死,比如把有 onclick 的元素全部干掉。对于 XSS,最安全的态度是「永远不要相信用户输入的数据」。
//使用 react-html-parser 后:
import ReactHtmlParser from 'react-html-parser';
let html = `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`;
function MyComponent() {
return <div>
{ ReactHtmlParser(html, {
transform: function transform (node) {
// 过滤 input 标签
if (node.type === 'input') {
return null;
}
}
}) }
<div/>;
}
小结
已上是作者在实践过程中遇到的问题,问题恐怕不止于此,但仅两点足以让我放弃直接使用 dangerouslySetInnerHTML
。这也正是 react 官方所提倡的做法,毕竟,这个属性的设计初衷就是要让开发者体会到「dangerous」。所以,再见吧,dangerouslySetInnerHTML
~
Congratulations @pobusama! You received a personal award!
Click here to view your Board of Honor
Congratulations @pobusama! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Vote for @Steemitboard as a witness to get one more award and increased upvotes!