介绍

有时我们想得到现有的 DOM 树序列化后的字符串,如果仅仅想得到指定节点的后代,可以直接用 Element.innerHTML 属性;如果你想得到包括节点本身及它所有的后代的话,可以使用 Element.outerHTML 属性。

如果将字符串内容解析为 DOM 树,则有几种方法。

字符串转 DOM

  1. innerHTML
    1
    2
    3
    4
    5
    function parse(html) {
    const placeholder = document.createElement("div");
    placeholder.innerHTML = html;
    return placeholder;
    }
  1. insertAdjacentHTML
    1
    2
    3
    4
    5
    function parse(html) {
    const placeholder = document.createElement("div");
    placeholder.insertAdjacentHTML("afterbegin", html);
    return placeholder;
    }
  1. DOMParser
    1
    2
    3
    4
    function parse(html) {
    const doc = new DOMParser().parseFromString(html, "text/html");
    return doc.body.childNodes;
    }
  1. createContextualFragment
    1
    2
    3
    4
    function parse(html) {
    const fragment = document.createRange().createContextualFragment(html);
    return fragment.childNodes;
    }

脚本执行

除了 createContextualFragment 之外,所有方法都会阻止常规脚本执行。例如:

1
2
const name = "<script>alert('I am John in an annoying alert!')</script>";
el.innerHTML = name; // harmless in this case

这并不会导致 XSS 攻击,因为在 HTML5 中不会执行由 innerHTML 插入的 script 脚本。

但是,有很多不依赖 script 标签去执行 JavaScript 的方式。例如:

1
2
const name = "<img src='x' onerror='alert(1)'>";
el.innerHTML = name; // shows the alert

上面的代码在浏览器中会执行 alert(1),其他类似的属性还有 onload,例如上面代码 onerror 换成 onloadsrc 改成正常的 URL 地址,在在浏览器中同样会执行。通过几种措施可以阻止这些情况:

  1. 可以在将实际节点附加到 DOM 之前去除子节点的所有违规属性:
    1
    2
    3
    [...placeholder.querySelectorAll("*")].forEach((node) =>
    node.removeAttribute("onerror");
    );
  2. 当插入纯文本时,建议使用 Node.textContent 代替 Element.innerHTML,它不会把给定的内容解析为 HTML,它仅仅是将原始文本插入给定的位置。
  3. 使用 Element.setHTML() 代替 Element.innerHTML,这是浏览器的一个最新方法,可以删除 HTML 字符串中在当前元素的上下文中任何不安全或无效的元素、属性或注释。
    1
    2
    3
    const unsanitized_string = '<img src="x" onerror="console.log(1)">';
    el.setHTML(unsanitized_string);
    console.log(el.innerHTML); // <img src="x">
  4. 使用 Sanitizer.sanitizeFor() 代替 Document.createElement() 创建新节点。该方法接收一个 HTML 标记名称,例如 div、table、p 等,和一个 HTML 字符串参数。返回一个解析和清理后的与参数中指定的标记相对应的 HTML 元素。
    1
    2
    3
    const 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
2
3
const placeholder = document.createElement("div");
placeholder.innerHTML = `<tr><td>Foo</td></tr>`;
const node = placeholder.firstElementChild; //=> null

但可以通过几种方法来避免这种情况:

  1. 通过 createContextualFragment 设置上下文:
    1
    2
    3
    4
    5
    6
    7
    8
    const 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
  2. 使用模板标签作为占位符,它没有任何内容限制:
    1
    2
    3
    4
    const template = document.createElement("template");
    template.innerHTML = `<tr><td>Foo</td></tr>`;
    const node = template.content;
    node.firstChild //=> tr
  3. 创建临时节点:
    1
    2
    3
    4
    5
    const tr=document.createElement('tr');
    tr.innerHTML = `<tr><td>Foo</td></tr>`;
    const placeholder = document.createElement("table");
    placeholder.appendChild(tr);
    placeholder.firstChild //=> tr
  4. 使用文档片段:
    1
    2
    3
    4
    5
    const tr=document.createElement('tr');
    tr.innerHTML = `<tr><td>Foo</td></tr>`;
    const fragment = new DocumentFragment();
    fragment.appendChild(tr);
    fragment.firstChild //=> tr

参考资料:

  1. 从 HTML 字符串创建 DOM 节点
  2. 盘点HTML字符串转DOM的各种方法及细节