安全地将外部内容插入页面

有时您可能希望或需要在扩展中包含来自外部来源的内容。但是,存在该来源可能嵌入恶意脚本的风险,这些脚本可能是由来源开发者或恶意第三方添加的。

以 RSS 阅读器为例。您不知道您的扩展将打开哪些 RSS 提要,也无法控制这些 RSS 提要的内容。因此,用户可能会订阅一个提要,其中提要项的标题包含一个脚本。这可能很简单,例如在<script></script>标签内包含 JavaScript 代码。如果您要提取标题,假设它是纯文本,并将其添加到扩展创建的页面的 DOM 中,您的用户现在将在他们的浏览器中运行一个未知脚本。因此,需要小心避免将任意文本评估为 HTML。

您还需要记住,扩展具有特权上下文,例如在后台脚本和内容脚本中。在最坏的情况下,嵌入的脚本可以在这些上下文中的一个中运行,这种情况称为权限提升。这种情况可能使用户的浏览器容易受到远程攻击,因为这将使注入代码的网站能够访问关键的用户数据,例如密码、浏览器历史记录或浏览行为。

本文介绍了如何安全地使用远程数据并将其添加到 DOM 中。

使用任意字符串

在处理字符串时,有两种推荐的安全将其添加到页面中的选项:标准 DOM 节点创建方法或 jQuery。

DOM 节点创建方法

将字符串插入页面的轻量级方法是使用本机 DOM 操作方法:document.createElementElement.setAttributeNode.textContent。安全的方法是分别创建节点并使用 textContent 为其分配内容

js
let data = JSON.parse(responseText);
let div = document.createElement("div");
div.className = data.className;
div.textContent = `Your favorite color is now ${data.color}`;
addonElement.appendChild(div);

这种方法是安全的,因为使用 .textContent 会自动转义 data.color 中的任何远程 HTML。

但是,请注意,您可以使用不安全的本机方法。请看以下代码

js
let data = JSON.parse(responseText);
addonElement.innerHTML = `<div class='${data.className}'>Your favorite color is now ${data.color}</div>`;

在这里,data.classNamedata.color 的内容可能包含 HTML,这些 HTML 可以提前关闭标签,插入任意的进一步 HTML 内容,然后打开另一个标签。

jQuery

在使用 jQuery 时,attr()text() 等函数在将内容添加到 DOM 时会转义内容。因此,上面示例中的“最喜欢的颜色”,在 jQuery 中实现如下

js
let node = $("</div>");
node.addClass(data.className);
node.text(`Your favorite color is now ${data.color}`);

使用 HTML 内容

在处理您知道是 HTML 的外部来源内容时,在将其添加到页面之前,对 HTML 进行消毒至关重要。消毒 HTML 的最佳实践是使用 HTML 消毒库或具有 HTML 消毒功能的模板引擎。在本节中,我们将介绍一些合适的工具及其使用方法。

HTML 消毒

HTML 消毒库会剥离任何可能导致脚本执行的 HTML,因此您可以安全地将完整的 HTML 节点集从远程来源注入到您的 DOM 中。DOMPurify 已由多位安全专家审查,是扩展中执行此任务的合适库。

用于生产用途的 DOMPurify 作为最小化版本提供:purify.min.js。您可以根据扩展的最佳方式使用此脚本。例如,您可以将其添加为内容脚本

json
"content_scripts": [
  {
    "matches" : ["<all_urls>"],
    "js": ["purify.min.js", "myinjectionscript.js"]
  }
]

然后,在 myinjectionscript.js 中,您可以读取外部 HTML、对其进行消毒并将其添加到页面的 DOM

js
let elem = document.createElement("div");
let cleanHTML = DOMPurify.sanitize(externalHTML);
elem.innerHTML = cleanHTML;

您可以使用任何方法将消毒后的 HTML 添加到您的 DOM 中,例如 jQuery 的 .html() 函数。但请记住,在这种情况下需要使用 SAFE_FOR_JQUERY 标志

js
let elem = $("<div/>");
let cleanHTML = DOMPurify.sanitize(externalHTML, { SAFE_FOR_JQUERY: true });
elem.html(cleanHTML);

模板引擎

另一种常见模式是为页面创建本地 HTML 模板并使用远程值填充空白。虽然这种方法通常是可以接受的,但应注意避免使用可能允许插入可执行代码的结构。当模板引擎使用插入原始 HTML 到文档中的结构时,可能会发生这种情况。如果用于插入原始 HTML 的变量来自远程来源,则它会受到引言中提到的相同安全风险的影响。

例如,在使用 mustache 模板 时,您必须使用双胡须 {{variable}},它会转义任何 HTML。必须避免使用三胡须 {{{variable}}},因为它会注入原始 HTML 字符串,并且可能在您的模板中添加可执行代码。Handlebars 的工作方式类似,双 handlebars 中的变量 {{variable}} 会被转义。而三 handlebars 中的变量则保持原始状态,必须避免使用。此外,如果您使用 Handlebars.SafeString 创建 Handlebars 帮助器,请使用 Handlebars.escapeExpression() 转义传递给帮助器的任何动态参数。这是必需的,因为来自 Handlebars.SafeString 的结果变量被认为是安全的,并且在使用双 handlebars 插入时不会被转义。

其他模板系统中存在类似的结构,需要以相同程度的谨慎对待。

其他阅读材料

有关此主题的更多信息,请参阅以下文章