您的富文本可能存在跨站点脚本漏洞

**在 SXSS 漏洞被利用之前识别并缓解漏洞**

作者:卢克·哈里森

当前许多应用程序都需要在其网站上以 HTML 格式呈现富文本。为了根据用户输入生成这种格式化的文本,开发人员使用富文本编辑器组件。问题是什么?此功能可能会间接使您的应用程序和数据面临称为存储型跨站点脚本 (SXSS) 的漏洞。

在本文中,您将了解什么是 SXSS 漏洞,并查看一些可用于检查应用程序是否受到影响的“代码异味”。您还将看到一个易受攻击的应用程序示例,并了解此漏洞的补救策略。

什么是存储型跨站点脚本?

存储型跨站脚本是一种漏洞,攻击者可利用该漏洞将恶意代码注入数据库。该代码在被前端框架获取和呈现后,在受害者的浏览器上运行。

此漏洞极其危险,因为它可使攻击者窃取 Cookie、触发重定向或在受害者的浏览器中运行各种危险脚本。攻击者几乎无需做任何工作即可传播漏洞:受害者无需点击恶意链接或落入钓鱼骗局,只需使用受 SXSS 影响的受信任站点即可。查看页面底部的链接,了解有关跨站点脚本漏洞的更多详细信息。

代码异味:innerHTML 和 dangerouslySetInnerHTML

A 只是代码中的一个特征,表明存在更深层次的问题。浏览器通常不会自动运行注入的脚本,但如果开发人员使用一些具有潜在危险的浏览器 API 或元素属性,则可能导致脚本运行的情况。

看一下下面的代码片段:

const someHTML = “

Hello world

“ const output = document.getElementById("rich-text-output"); output.innerHTML = someHTML

在此示例中,我们将一些 HTML 存储在变量中,从 DOM 中获取元素,并将该元素的 innerHTML 属性设置为存储在变量中的内容。innerHTML 属性可用于从另一个 HTML 元素内的字符串呈现 HTML。

此属性的危险之处在于,它会呈现您传递给它的任何 HTML 或 JavaScript。这意味着,如果有人能够控制传递给该属性的数据,那么从技术上讲,他们就可以在用户的​​浏览器中运行任何 JavaScript。

在浏览器中呈现动态 HTML 的另一种流行但危险的方法是使用 dangerlySetInnerHTML React 组件属性。此属性的行为与原始 JavaScript 和 HTML 中的 innerHTML 属性完全相同。

以下示例出现在 React 文档中:

function createMarkup() {

  return {__html: 'First · Second'};

}

function MyComponent() {

  return 
; }

如果您当前在前端 Web 应用程序中使用这些属性中的任何一个,则很有可能存在某种类型的跨站点脚本漏洞。我们将在本文后面介绍如何利用这些属性以及您可以采取哪些步骤来补救这些问题。

代码异味:富文本编辑器

您的应用程序可能容易受到 SXSS 攻击的另一个迹象是您是否使用富文本编辑器,例如 TinyMCE 或 CKEditor。

TinyMCECKEditor

大多数富文本编辑器的工作方式是将用户生成的格式化文本转换为 HTML。作为一项额外的安全措施,许多此类编辑器都采用某种形式的清理措施,从输入中删除潜在的恶意 JavaScript。但是,如果您没有在接收和存储富文本内容的服务上应用这些相同的清理技术,那么您的应用程序很可能会受到 SXSS 的攻击。

即使您没有在自己的网站上渲染内容,渲染应用程序也很有可能使用这些数据。要设计安全的应用程序,考虑数据的当前和未来消费者非常重要。如果您的数据受到 SXSS 的影响,那么使用您数据的所有应用程序也会受到影响。

存在 SXSS 漏洞的示例应用程序

让我们看一个存在 SXSS 漏洞的 Web 应用程序的小示例,然后尝试利用它。

要运行此应用程序,首先克隆此演示应用程序 repo,并按照“readme.md”文件中的“运行应用程序”说明进行操作。

运行应用程序并转到 http://localhost:3000/unsanitized.html 后,您应该看到如下页面:

vulnerable web app

该应用程序只需从用户那里获取一些富文本输入,将其存储在 Web 服务器上,然后在标有 **输出** 的部分中呈现它。

在利用 SXSS 漏洞之前,请花点时间查看一下应用程序。参考上面提到的代码异味并扫描代码,看看是否能发现有问题的部分。尝试在浏览器中打开网络选项卡,查看输入和提交一些富文本时它发送的请求。

在 `unsanitzed.html` 文件中,您将看到以下名为 `renderPostByID` 的函数:

const renderPostByID = async (id) => {
    // setting url seach params
    let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + `?post=${id}`;
    window.history.pushState({ path: newURL }, "", newURL);

    // getting rich text by post id
    let response = await fetch(`/unsanitized/${id}`, { method: "GET" });
    let responseJSON = await response.json();
    console.log(responseJSON);

    // rendering rich text
    output.innerHTML = responseJSON.richText;
};

仔细查看此函数。您会注意到,我们正在使用上述“innerHTML”属性以 HTML 形式呈现从 API 获取的一些富文本。

现在我们看到了代码中易受攻击的部分,让我们利用它。我们将绕过富文本编辑器输入并直接点击将帖子保存到 Web 服务器的 API 端点。为此,您可以使用以下 cURL 命令:

curl --request POST \
--url http://localhost:3000/unsanitzed \
--header 'Content-Type: application/json' \
--data '{
"richText": ""
}'

注意我们在请求中发送的数据负载。这是一些恶意制作的 HTML,其中包含一个图像标记,该标记的“onerror”属性设置为显示警报对话框的 JavaScript。攻击者将使用此类技巧来避免实施不当的清理方法,这些方法旨在在将 HTML 元素存储到数据库之前从 HTML 元素中删除 JavaScript。

运行上述脚本后,您应该收到如下帖子 ID:

{"id":"1efa54c3-4d13-6c80-b1e8-a942fe26c532"}

将此帖子 ID 粘贴到帖子 URL 查询参数中,然后按 **Enter**。

post id

当您执行此操作时,您应该会在屏幕上看到一个警告对话框,确认该站点确实容易受到 SXSS 攻击。

alert dialog

如何预防SXSS

现在我们已经了解了如何利用 SXSS 漏洞,让我们看看如何利用它。为此,您需要在三个不同的地方清理基于 HTML 的富文本:

  • 服务器端,在内容存储到数据库之前。
  • 服务器端,当从数据库检索内容时。
  • 客户端,当内容由浏览器呈现时。
  • 您可能很清楚为什么要在将内容存储到数据库之前以及在客户端呈现内容时对其进行清理,但为什么要在检索内容时进行清理呢?好吧,让我们假设有人获得了将内容直接插入数据库所需的权限。他们现在可以直接插入一些恶意制作的 HTML,完全绕过初始清理程序。如果您的某个 API 的使用者没有在客户端实施此清理,他们可能会成为跨站点脚本攻击的受害者。

    但请记住,在所有三个位置添加清理可能会导致性能下降,因此您需要自行决定是否需要这种级别的安全性。至少,您应该在呈现动态 HTML 内容之前清理客户端上的所有数据。

    让我们看看如何在易受攻击的应用程序的安全版本中实现清理。由于此应用程序主要使用 JavaScript 编写,因此我们使用 dompurify 库作为客户端,使用 isomorphic-dompurify 库进行服务器端清理。在充当我们的 Web 服务器的 `app.js` 程序中,您将找到一个带有 GET 和 POST 实现的快速端点 `/sanitized`:

    app.post("/sanitized", (req, res) => {
    
      let richText = req.body.richText;
    
      let id = uuid.v6();
    
      data[id] = DOMPurify.sanitize(richText); // insert sanitized input
    
      res.json({ id });
    
    });
    
    app.get("/sanitized/:id", (req, res) => {
    
      let id = req.params.id;
    
      let richText = DOMPurify.sanitize(data[id]); // retrieve sanitized input
    
      res.json({ richText });
    
    });

    在 POST 实现中,我们首先从请求正文中检索富文本,然后调用 isomorphic-dompurify 库的 sanitize 方法,然后将其存储在我们的数据对象中。同样,在 GET 实现中,我们在从我们的数据对象中检索富文本之后,将其发送给我们的消费者之前,对富文本调用相同的方法。

    在客户端,我们在“sanitized.html”中设置输出 div 的 innerHTML 属性之前再次使用相同的方法。

    // rendering rich text
    output.innerHTML = DOMPurify.sanitize(responseJSON.richText);

    现在您已经了解了我们如何正确清理 HTML 以防止跨站点脚本,请返回此应用程序的原始漏洞并再次运行它,这次使用已清理的端点。您应该不再看到警报对话框弹出,因为我们现在正在使用正确的技术来防止 SXSS 漏洞。

    有关完整的 SXSS 指南,包括最佳实践和其他防止 XSS 的技术,请查看 OWASP 跨站点脚本备忘单。

    总结和后续步骤

    在本文中,我们研究了如何通过阻止存储型跨站点脚本(一种常见的 Web 应用程序漏洞)来提高应用程序的安全状况。现在,您应该能够识别自己的应用程序是否存在漏洞、需要检查哪些功能以及如何在恶意行为者利用这些漏洞之前采取缓解措施。

    安全对于企业开发人员来说至关重要。使用以下资源继续提高您对潜在漏洞的认识,并了解改善安全状况的方法。

  • IBM 开发人员:安全中心
  • OWASP 跨站点脚本概述
  • 视频:跨站脚本攻击——25 年来仍未消退的威胁