安全地将外部内容插入页面
有时您可能希望或需要在扩展中包含来自外部来源的内容。但是,存在该来源可能嵌入恶意脚本的风险,这些脚本可能是由来源开发者或恶意第三方添加的。
以 RSS 阅读器为例。您不知道您的扩展将打开哪些 RSS 提要,也无法控制这些 RSS 提要的内容。因此,用户可能会订阅一个提要,其中提要项的标题包含一个脚本。这可能很简单,例如在<script></script>标签内包含 JavaScript 代码。如果您要提取标题,假设它是纯文本,并将其添加到扩展创建的页面的 DOM 中,您的用户现在将在他们的浏览器中运行一个未知脚本。因此,需要小心避免将任意文本评估为 HTML。
您还需要记住,扩展具有特权上下文,例如在后台脚本和内容脚本中。在最坏的情况下,嵌入的脚本可以在这些上下文中的一个中运行,这种情况称为权限提升。这种情况可能使用户的浏览器容易受到远程攻击,因为这将使注入代码的网站能够访问关键的用户数据,例如密码、浏览器历史记录或浏览行为。
本文介绍了如何安全地使用远程数据并将其添加到 DOM 中。
使用任意字符串
在处理字符串时,有两种推荐的安全将其添加到页面中的选项:标准 DOM 节点创建方法或 jQuery。
DOM 节点创建方法
将字符串插入页面的轻量级方法是使用本机 DOM 操作方法:document.createElement
、Element.setAttribute
和 Node.textContent
。安全的方法是分别创建节点并使用 textContent 为其分配内容
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。
但是,请注意,您可以使用不安全的本机方法。请看以下代码
let data = JSON.parse(responseText);
addonElement.innerHTML = `<div class='${data.className}'>Your favorite color is now ${data.color}</div>`;
在这里,data.className
或 data.color
的内容可能包含 HTML,这些 HTML 可以提前关闭标签,插入任意的进一步 HTML 内容,然后打开另一个标签。
jQuery
在使用 jQuery 时,attr()
和 text()
等函数在将内容添加到 DOM 时会转义内容。因此,上面示例中的“最喜欢的颜色”,在 jQuery 中实现如下
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。您可以根据扩展的最佳方式使用此脚本。例如,您可以将其添加为内容脚本
"content_scripts": [
{
"matches" : ["<all_urls>"],
"js": ["purify.min.js", "myinjectionscript.js"]
}
]
然后,在 myinjectionscript.js 中,您可以读取外部 HTML、对其进行消毒并将其添加到页面的 DOM
let elem = document.createElement("div");
let cleanHTML = DOMPurify.sanitize(externalHTML);
elem.innerHTML = cleanHTML;
您可以使用任何方法将消毒后的 HTML 添加到您的 DOM 中,例如 jQuery 的 .html()
函数。但请记住,在这种情况下需要使用 SAFE_FOR_JQUERY
标志
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 插入时不会被转义。
其他模板系统中存在类似的结构,需要以相同程度的谨慎对待。
其他阅读材料
有关此主题的更多信息,请参阅以下文章