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

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

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

您还需要记住,扩展程序具有特权上下文,例如在后台脚本和内容脚本中。在最坏的情况下,嵌入的脚本可能会在这些上下文之一中运行,这种情况称为权限升级。这种情况可能使攻击者能够通过允许注入代码的网站访问关键用户数据(如密码、浏览器历史记录或浏览行为)来让用户的浏览器遭受远程攻击。

本文探讨了如何安全地处理远程数据并将其添加到 DOM。

处理任意字符串

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

用于节点创建和安全文本插入的 DOM API

要轻量级且安全地插入字符串,请使用原生 DOM API:使用 document.createElement 创建元素,并仅使用 Element.setAttribute 设置已验证的、不可执行的属性。要添加文本内容,请使用 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 内容,然后打开另一个标签。

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 节点集从远程源注入到您的 DOM 中。DOMPurify 经过多位安全专家审查,是扩展程序中用于此任务的合适库。

对于生产环境,DOMPurify 提供了一个精简版本:purify.min.js。您可以根据扩展程序的需要使用此脚本。例如,您可以将其添加为内容脚本

json
"content_scripts": [
  {
    "matches" : ["<all_urls>"],
    "js": ["purify.min.js", "my-injection-script.js"]
  }
]

然后,在 my-injection-script.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 的工作方式类似,双大括号 {{variable}} 中的变量会被转义。而三层大括号中的变量将保持原样,必须避免。此外,如果您使用 Handlebars.SafeString 创建 Handlebars 助手,请使用 Handlebars.escapeExpression() 转义传递给助手的任何动态参数。这是必需的,因为 Handlebars.SafeString 生成的变量被认为是安全的,并且在用双大括号插入时不会被转义。

其他模板系统也有类似的构造,需要同样小心地处理。

延伸阅读

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