介绍
有时我们想得到现有的 DOM 树序列化后的字符串,如果仅仅想得到指定节点的后代,可以直接用 Element.innerHTML
属性;如果你想得到包括节点本身及它所有的后代的话,可以使用 Element.outerHTML
属性。
如果将字符串内容解析为 DOM 树,则有几种方法。
字符串转 DOM
- innerHTML
1
2
3
4
5function parse(html) {
const placeholder = document.createElement("div");
placeholder.innerHTML = html;
return placeholder;
}
- insertAdjacentHTML
1
2
3
4
5function parse(html) {
const placeholder = document.createElement("div");
placeholder.insertAdjacentHTML("afterbegin", html);
return placeholder;
}
- DOMParser
1
2
3
4function parse(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.childNodes;
}
- createContextualFragment
1
2
3
4function parse(html) {
const fragment = document.createRange().createContextualFragment(html);
return fragment.childNodes;
}
- 安全:会执行脚本
- 允许的节点:可以设置上下文允许的节点
脚本执行
除了 createContextualFragment
之外,所有方法都会阻止常规脚本执行。例如:
1 | const name = "<script>alert('I am John in an annoying alert!')</script>"; |
这并不会导致 XSS 攻击,因为在 HTML5 中不会执行由 innerHTML
插入的 script 脚本。
但是,有很多不依赖 script 标签去执行 JavaScript 的方式。例如:
1 | const name = "<img src='x' onerror='alert(1)'>"; |
上面的代码在浏览器中会执行 alert(1)
,其他类似的属性还有 onload
,例如上面代码 onerror
换成 onload
,src
改成正常的 URL 地址,在在浏览器中同样会执行。通过几种措施可以阻止这些情况:
- 可以在将实际节点附加到 DOM 之前去除子节点的所有违规属性:
1
2
3[...placeholder.querySelectorAll("*")].forEach((node) =>
node.removeAttribute("onerror");
); - 当插入纯文本时,建议使用
Node.textContent
代替Element.innerHTML
,它不会把给定的内容解析为 HTML,它仅仅是将原始文本插入给定的位置。 - 使用
Element.setHTML()
代替Element.innerHTML
,这是浏览器的一个最新方法,可以删除 HTML 字符串中在当前元素的上下文中任何不安全或无效的元素、属性或注释。1
2
3const unsanitized_string = '<img src="x" onerror="console.log(1)">';
el.setHTML(unsanitized_string);
console.log(el.innerHTML); // <img src="x"> - 使用
Sanitizer.sanitizeFor()
代替Document.createElement()
创建新节点。该方法接收一个 HTML 标记名称,例如 div、table、p 等,和一个 HTML 字符串参数。返回一个解析和清理后的与参数中指定的标记相对应的 HTML 元素。1
2
3const unsanitized_string = '<img src="x" onerror="console.log(1)">';
const p = new Sanitizer().sanitizeFor('p', unsanitized_string);
console.log(p.innerHTML); // <img src="x">
HTML 限制
HTML 中有一些限制会阻止将某些类型的节点添加到像 div 这样的节点,例如 thead、tbody、tr 和 td。
1 | const placeholder = document.createElement("div"); |
但可以通过几种方法来避免这种情况:
- 通过 createContextualFragment 设置上下文:
1
2
3
4
5
6
7
8const table = document.createElement(`table`);
const tbody = document.createElement(`tbody`);
table.appendChild(tbody);
const range = document.createRange();
range.selectNodeContents(tbody);
const node = range.createContextualFragment(`<tr><td>Foo</td></tr>`);
node.firstChild //=> tr - 使用模板标签作为占位符,它没有任何内容限制:
1
2
3
4const template = document.createElement("template");
template.innerHTML = `<tr><td>Foo</td></tr>`;
const node = template.content;
node.firstChild //=> tr - 创建临时节点:
1
2
3
4
5const tr=document.createElement('tr');
tr.innerHTML = `<tr><td>Foo</td></tr>`;
const placeholder = document.createElement("table");
placeholder.appendChild(tr);
placeholder.firstChild //=> tr - 使用文档片段:
1
2
3
4
5const tr=document.createElement('tr');
tr.innerHTML = `<tr><td>Foo</td></tr>`;
const fragment = new DocumentFragment();
fragment.appendChild(tr);
fragment.firstChild //=> tr
参考资料:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 张坤的博客!
评论