2023年10月19日
阅读时间:13分钟
原文:https://snyk.io/blog/github-copilot-xss-react/
在人工智能(AI)和大型语言模型(LLMs)不断发展的时代,GitHub Copilot等创新工具正在改变软件开发的格局。在先前的一篇文章中,我谈到了这种变革的影响,以及它如何延伸到这些智能自动化工具所提供的便利性,以及它给我们编码实践中维护强大安全性带来的新一套挑战。Snyk还发布了关于使用AI编码时的安全风险的案例研究。在这篇文章中,我们旨在探讨在React代码库中使用GitHub Copilot时的安全性方面,以及它如何在前端开发人员的React组件JSX文件中为他们自动补全代码。我们的目标是检查GitHub Copilot提出的代码是否符合安全编码原则,帮助开发人员编写成功规避潜在跨站脚本(XSS)漏洞的代码,尤其在React开发环境中。对于不熟悉XSS严重后果的人来说,它们基本上允许攻击者将客户端脚本注入其他用户查看的网页中。
Developers adopt GitHub Copilot
开发人员采用 GitHub Copilot 在人工智能对各个领域产生影响的领域中,软件开发占据了一个值得注意的位置。在这一领域中出现的新兴创新之一是 GitHub Copilot,这是一款开发人员工具,内置在开发环境中,例如 VS Code,由 OpenAI 的 Codex 提供支持。GitHub Copilot 的设计是作为您的人工智能协作开发者。它能够提供编码建议,编写整行或整块代码,并兼容多种编程语言 — 这种功能也适用于流行的前端库和框架,比如 React。随着 Copilot 在我们的开发者工具箱中变得越来越受到依赖,我们必须能够信任它所建议的代码的语法,但我们还需要确保这段代码足够安全,能够符合我们所实践的标准,并且不会危害用户。通过 JavaScript 代码示例和对 JSX 和 React 特定安全实践的深入研究,本文旨在不仅仅是为了通知,而且为了让开发人员评估和利用这些经过人工智能增强的开发工具,而不会损害应用程序安全。
危险区域:使用React的dangerouslySetInnerHTML函数React提供了一个API,让开发者可以直接从React组件中设置HTML — dangerouslySetInnerHTML函数。正如其名称所示,使用这个函数可能会很危险。它的工作原理是绕过React中执行输出编码的安全控制,以帮助防止跨站脚本攻击,允许您手动将HTML插入组件中。这个特性对于某些情况很有用,比如处理来自受信任来源的富文本内容。例如:
1function MyComponent() {
2 return <div dangerouslySetInnerHTML={{__html: '<h1>Hello Worldh1>'}} />;
3}
尽管这提供了一种快速直接的处理 HTML 内容的方式,但也会将您的应用程序暴露于跨站脚本 (XSS) 攻击的风险之下。假设用户提供的数据是 `dangerouslySetInnerHTML` 设置的 HTML 内容的一部分,攻击者可以注入一个将被执行的任意脚本 — 潜在影响可能非常广泛。
1function MyComponent({userInput}) {
2 // This can expose the application to XSS risks if userInput contains a malicious script.
3 return <div dangerouslySetInnerHTML={{__html: userInput}} />;
4}
因此,在应用程序中使用 `dangerouslySetInnerHTML` 时必须谨慎。
GitHub Copilot是否提供安全的代码建议?
GitHub Copilot 可以帮助开发人员实现一些安全控制,以减轻在 `dangerouslySetInnerHTML` 中发生 XSS 的可能性吗?让我们考虑以下使用 `dangerouslySetInnerHTML` 指令的 React 组件代码:
1 "justify-content-between">
2 <Col md="6">
3 <Row className="justify-content-between align-items-center">
4 <div
5 dangerouslySetInnerHTML={{
6 __html: `
7 <img src=${database.authorScreenshotURL}
8 alt=${
9 authorScreenshotDescription
10 } />
11 `,
12 }}
13 />
14 Row>
流入此组件的变量 `authorScreenshotDescription` 是由用户控制的,用于指定图像的文本描述。攻击者可能利用这个跨站脚本漏洞,在浏览器中通过将 `authorScreenshotDescription` 变量的值设置为以下内容来实现 JavaScript 代码执行: `"
那么接下来怎么办呢?您开始为这个 React 组件编写一个快速的跨站脚本转义函数。当然,GitHub Copilot 在那里帮助您。
当您开始输入函数名称及其参数时,正要开始编写函数体时,GitHub Copilot会自动建议以下代码。看起来很不错。您只需按TAB键即可。当然,别忘了更新我们在组件中使用`dangerouslySetInnerHTML` API,以使用此安全转义函数:
尝试相同的 XSS 攻击,攻击者之前成功的攻击。我们发现它失败了。
显然,GitHub Copilot建议的代码自动补全`escapeCrossSiteScripting`函数效果神奇,确实转义了先前创建了新的`
` HTML元素并执行JavaScript代码的尖括号。
我们还没有走出危险。
攻击者是坚持不懈的,对他们来说几乎没有成本可自动化他们的攻击有效负载。因此,他们可能会通过数千种字符串排列的迭代,通常被称为模糊测试,在找到有效的一种方式之前进行尝试。这就是计算机黑客技术发展的地方 - 开发人员需要从每个潜在故障点保护,而攻击者只需要找到一种方式进入,尽管它很微小。
因此,攻击者可能尝试以下有效负载:
1s \"
在上述情况下,我们将`onError`属性处理程序更改为`onLoad`处理程序。现在,如果我们再次加载网页,我们将观察到这次攻击成功,弹出了一个弹窗:
作为开发人员,您会想知道这种安全漏洞是如何发生的,以及如何避免它。一位经验丰富的 React 开发人员可能会告诉您,在代码安全性方面,将属性的值用引号括起来是一种更优越的编码约定,因此让我们这样做,并在下面的 `alt=` 属性值中应用此更改:
1 2 dangerouslySetInnerHTML={{
3 __html: `
4
${database.authorScreenshotURL}
5 alt="${escapeCrossSiteScripting(
6 authorScreenshotDescription
7 )}" />
8 `,
9 }}
10 />
这样做并应用先前起作用的攻击有效负载`s "
看起来很棒。
直到...
攻击者找到了一种创造性的方式,以一个更短的负载形式逃离这种情况,保持 `onLoad` 特殊属性的滥用,并注入一个分号和一个 `//` 字符串,表示任何尾随字符串应被视为注释。使用以下负载:
1s \" onLoad=alert(1); //
欢迎来到另一次成功执行跨站脚本攻击:
但是如果我们尝试不同的方法呢?如果我们不在 'dangerouslySetInnerHTML' 部分用双引号括住 `alt=` 属性,而是尝试彻底重构转义函数呢?让我们尝试这样做。所以,我们的 React 组件的 JSX 代码仍然如下所示:
1 2 dangerouslySetInnerHTML={{
3 __html: `
4
${database.authorScreenshotURL}
5 alt=${escapeHTML(
6 authorScreenshotDescription
7 )} />
8 `,
9 }}
10 />
接下来,我们想重构现有的 'escapeCrossSiteScripting',使其成为一个更安全、更详尽地处理字符编码的HTML转义函数,而不仅仅是 '' 和 '&'。所以,我们开始输入代码,当然,GitHub Copilot 醒来并提供以下建议:
它实际上建议整个函数体代码,我只是简单地提示它继续完成代码建议,直到我们完成这个净化。看起来像是一个更好的输出编码逻辑,考虑了其他危险字符,如单引号和双引号。
如果我们向应用程序提供我们的原始有效负载:
1s \"
GitHub Copilot 提供的代码确实对所有潜在危险字符进行了编码,包括攻击有效荷载试图逃逸的双引号:
然而,我们又错了,因为如下简单的攻击载荷仍会触发跨站脚本,从而允许执行任何 JavaScript 代码
然而,我们还是会再次错了,因为如下简单的攻击有效负载仍然会触发跨站脚本,从而允许执行任何 JavaScript 代码:
1s onLoad=alert(1)
为什么会发生这种情况呢?