跳转至内容

网络应用程序安全指南/跨站脚本 (XSS)

来自维基教科书,开放世界中的开放书籍

跨站脚本 (XSS)

如果 web 应用程序输出中包含的用户输入没有正确转义,就会出现 XSS 漏洞。这种漏洞允许攻击者向 web 应用程序输出中注入内容。这可以用于注入虚假登录表单(向攻击者报告输入)或恶意 JavaScript 代码,这些代码可以窃取 cookie 和信息,或使用用户的权限执行操作。XSS 漏洞分为两大类,即*反射*(非持久)和*持久*漏洞。

反射型 XSS 漏洞仅在请求后直接在输出中包含用户输入。因此,攻击者需要用户点击恶意链接或发出恶意 POST 请求。前者可以通过将链接作为 IFRAME 包含来完成;后者可以通过 JavaScript 完成。这两种漏洞都需要用户访问恶意/受损的网站,但它们并不一定需要用户交互。

持久型 XSS 漏洞存储用户输入,并在以后的输出中包含它(例如,在论坛中的帖子)。这意味着用户不需要访问恶意/受损的网站。

为了防止这种类型的攻击

  • 在将任何非常量内容包含在响应中之前对其进行转义,尽可能靠近输出(即在包含“echo”或“print”调用的行中)
  • 如果不可能(例如,在构建更大的 HTML 块时),在构建时进行转义,并在名称中表明变量内容是预转义的,以及预期的上下文
  • 在转义时考虑上下文:在 HTML 中转义文本与转义 HTML 属性值不同,与转义 CSS 或 JavaScript 中的值或 HTTP 头中的值也大不相同。
    • 这意味着你可能需要针对多个上下文和/或多次进行转义。例如,当将 HTML 片段作为 JS 常量传递以供稍后包含在文档中时,你需要在将常量写入 JavaScript 源代码时针对 HTML 中的 JS 字符串进行转义,然后在你的脚本将片段写入文档时再次针对 HTML 进行转义。(见理由中的例子)
    • 攻击者不应能够将任何内容放到不应该放置的地方,即使你认为它不可利用(例如,因为尝试利用它会导致 JavaScript 崩溃)。
  • 在文档开头(即尽快)和/或在头部显式设置正确的字符集。
  • 确保用户提供的 URL 以允许的方案开头(白名单),以避免危险方案(例如,javascript:-URL)
  • 不要忘记重定向脚本中的 URL
  • 可以将 内容安全策略 用作额外的安全措施,但它本身不足以防止攻击。

理由

在输出位置直接转义数据,可以更容易地检查所有输出是否都已转义 - 每个用作输出方法参数的变量,要么必须标记为预转义,要么必须用相应的转义命令包装。

不同的上下文需要完全不同的转义规则。一个“)”字符在 HTML 和 HTML 属性中没有危险含义,但在 CSS 中可以表示 URL 路径的结束。见下面的例子,它展示了一个复杂但常见的用例,其中 HTML 和 JavaScript 一起使用,并为 XSS 创造了无数的机会。请注意,即使是错误的转义,也会“意外地”阻止许多简单的 XSS 尝试(例如,HTML 转义会破坏 JavaScript 字符串注入所需的引号,或者换行符在注入尝试的情况下会创建无效的 JavaScript)。不要依赖此。攻击者可能知道一个你没有想到的技巧。如果可以在文档结构中任何不应放置的地方放置任何内容(例如,在 JavaScript 字符串文字之外),则这是一个必须修复的安全问题。它可能不可利用 - 或者你可能只是没有看到利用它的方法。不要冒险!

未设置字符集可能会导致浏览器猜测。这种猜测可以被利用来传递一个在你的预期编码中看起来无害的字符串,但在浏览器假定的编码中会被解释为一个脚本标签。对于 HTML5,请在头部部分使用<meta charset="utf-8" /> 作为第一个元素。

URL 也可能很危险。用户提供的链接应该与方案白名单进行比较,因为 javascript 方案不是唯一危险的方案。其他方案可能会触发可能不需要的操作。如果只允许 web 链接,则要求 URL 以“http://”或“https://”开头。

一个 内容安全策略 可以阻止某些类型的注入。只有某些浏览器支持它;其他浏览器只是忽略它。它是一种强大的辅助防御,可以限制安全问题的影响,但不能用作防止 XSS 的主要方法 - 防止 XSS 的主要方法是正确的转义,这不仅可以防止 XSS,还可以确保你的页面即使在存在不常见输入的情况下也能正确显示。实施 CSP 可能需要对你的代码进行重大更改。值得注意的是,你不能包含任何内联 JavaScript(除非你在你的 CSP 中显式允许内联 JS - 这将消除 CSP 提供的大部分保护)。

包含 JS 的复杂 XSS 示例

经常被忽视的问题包括 HTML 和 JavaScript 之间的复杂交互。一种常用的构造方法类似于以下内容

<script>
  var CURRENT_VALUE = 'test';
  document.getElementById("valueBox").innerHTML = CURRENT_VALUE; // INSECURE CODE - DO NOT USE.
</script>

CURRENT_VALUE 的内容(在本例中为单词test)根据例如用户输入或数据库中的值由服务器动态插入页面源代码中。第二行(实际将数据写入文档的代码)通常是包含在文件中的脚本的一部分。除非在每一步都使用适当的转义,否则有很多方法可以针对这种构造进行 XSS 攻击。在我们的例子中,攻击者想要执行代码alert(1);

首先,如果缺少 JavaScript 的适当转义,攻击者只需提供适当的引号符号来终止字符串,一个分号,他的代码,然后注释掉剩下的代码行。例如,攻击者可以提供值';alert(1);//,从而产生以下 HTML 代码,执行他的代码

<script>
  var CURRENT_VALUE = '';alert(1);//';
  document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>

请注意,即使使用像htmlspecialchars() 这样的 HTML 转义函数对值进行转义,这也能正常工作,如果该函数没有处理本例中使用的单引号。

假设攻击者不能使用适当的引号,因为它被过滤了,他可以使用值</script><script>alert(1);</script>。在常规 JavaScript 文件中,生成的代码行不会立即引起问题(尽管将其分配给 innerHTML 会引起问题),因为以下是一个完全安全的变量赋值

var CURRENT_VALUE = '</script><script>alert(1);</script>';

但是,由于这出现在内联脚本块中,HTML 解析器会解释“script-end”标签,从而导致一个损坏的 JavaScript 代码片段,后面跟着一个包含攻击者代码、一些文本和一个伪造的 script-end 标签的第二个脚本块

<script>
  var CURRENT_VALUE = '</script><script>alert(1);</script>';
  document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>

或者,为了清晰起见,重新缩进

<script>
  var CURRENT_VALUE = '
</script>
<script>alert(1);</script>
'; document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>

攻击者还可以简单地在字符串末尾插入一个反斜杠来破坏 JavaScript,从而转义字符串末尾的引号

var CURRENT_VALUE = 'text\';

字符串中的任何地方的简单换行符也会导致语法错误(未终止的字符串文字)。虽然这些攻击在本例中不允许直接进行 XSS,但它们可能会破坏关键的安全功能,使网站无法使用(拒绝服务),或者如果另一个值可以被操纵,则允许 XSS - 在此,攻击者向该构造的变体提供text\;alert(1);',该变体传递了两个值

var CURRENT_VALUE1 = 'text\'; var CURRENT_VALUE2 = ';alert(1);'';

由于字符串结束的引号被转义了,因此应该开始第二个字符串的引号反而关闭了第一个字符串,将剩余的内容变成了 JavaScript。这使我们回到了上面的陈述:如果可以在文档结构中任何不应放置的地方放置任何内容(例如,在 JavaScript 字符串文字之外),则这是一个必须修复的安全问题。它可能不可利用 - 或者你可能只是没有看到利用它的方法。不要冒险!

这些仅仅是我们示例中第一行存在的问题。第二行直接将值作为 HTML 插入文档,从而允许 XSS。为了利用这一点,攻击者必须由于上述问题而避免使用脚本结束标签,因此他使用一个带有错误处理程序的非现有图像。他的输入<img src=1 onerror=alert(1)> 导致

<script>
  var CURRENT_VALUE = '<img src=1 onerror=alert(1)>';
  document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>

innerHTML 赋值将图像标签放入文档中,并且由于“1”不是有效的 URL,因此会执行错误处理程序。请注意,这不是完全有效的 HTML,因为缺少属性周围的引号。它仍然有效到足以工作,并且避免了由于转义而使引号变得混乱。

在服务器端(在将其写入变量赋值行时)简单地使用像htmlspecialchars() 这样的函数对输出值进行 HTML 转义,可以防止其中一些攻击,并可能使其他攻击变得不可利用或更难利用。但是,这样做是不正确且危险的,并将留下其他攻击手段!

最值得注意的是,攻击者可能会决定做你应该做的事情,并为你正确地转义他的攻击序列。这将留下反斜杠\ 作为唯一的特殊字符,给出像\u003Cimg src=1 onerror=alert(1)\u003E 这样的输入(注意任何剩余的字符,即空格、大括号、等号和字母也可以被转义)。这不会受到你的转义函数的伤害,从而导致以下代码

<script>
  var CURRENT_VALUE = '\u003Cimg src=1 onerror=alert(1)\u003E';
  document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>

JavaScript 解析器会解释转义序列并将 XSS 代码插入你的文档中。


在这种情况下,有两种正确的方式进行转义

  • 方法 1 - 在服务器端进行 JS 转义,在客户端进行 HTML 转义(推荐)
    • 在服务器上,使用 JavaScript 转义值正确地(见下文)转义。
    • 在客户端的 JavaScript 中,确保在将文本插入文档之前转义文本,例如使用 jQuery 的 .text() setter
  • 方法 2 - 在服务器端进行 HTML 转义,在客户端进行 JS 转义(不推荐)
    • 在服务器上,首先对 HTML 进行转义。
    • 在服务器上,然后在将值插入文档之前,正确地(见下文)使用 JavaScript 转义值进行转义。

方法 2 允许您将服务器生成的自定义 HTML 传递到客户端。您需要像任何其他 HTML 输出一样转义 HTML(例如,使用 PHP 中的 htmlspecialchars)。然后,转义的内容将被传递到客户端,客户端会直接将其转储到文档中。这意味着客户端无法将文本用于任何非 HTML 上下文,尝试这样做可能会导致安全问题。正如您所看到的,转义是按相反顺序进行的:最后解释的格式(在本例中为 HTML)首先进行转义,然后整个字符串被外部格式的转义“包装”。

推荐的方法是在文本准备好输出之前保持未转义,然后在输出之前进行转义(即知道上下文时)。始终如一地遵循这种方法还可以避免双重编码(即在文本中向用户显示 & 等 HTML 实体)。

如何在 HTML 中正确地对 JavaScript 进行转义:确保像 < 这样的字符,它们在 JavaScript 中没有特殊含义,但在 HTML 中有特殊含义,也得到转义。不要编写自己的转义例程,您很可能会遗漏一些内容。使用现有的库。对于当前版本的 PHP,您可能需要考虑使用带有额外标志集的 json_encode()

...
<script>
  var CURRENT_VALUE = <?php echo json_encode($text,
        JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS); ?>;
    $("#valueBox").text(CURRENT_VALUE);
</script>
...

即使文本包含奇怪的特殊字符,现在也能正确渲染。

华夏公益教科书