跳到内容

跨站脚本攻击预防速查表

简介

本速查表旨在帮助开发人员预防 XSS 漏洞。

跨站脚本攻击(XSS)是一个误称。最初,此术语来源于攻击的早期版本,主要关注跨站窃取数据。此后,该术语的范围扩大到包括基本上任何内容的注入。XSS 攻击是严重的,可能导致账户冒充、观察用户行为、加载外部内容、窃取敏感数据等等。

本速查表包含预防或限制 XSS 影响的技术。由于没有单一的技术可以解决 XSS,因此需要使用正确的防御技术组合来预防 XSS。

框架安全性

幸运的是,使用现代 Web 框架构建的应用程序 XSS 漏洞较少,因为这些框架引导开发人员遵循良好的安全实践,并通过使用模板、自动转义等方式帮助缓解 XSS。然而,开发人员需要知道,如果框架使用不安全,例如以下情况,仍然可能出现问题:

  • 框架用于直接操作 DOM 的逃生舱口
  • React 的dangerouslySetInnerHTML未对 HTML 进行清理
  • React 无法在没有专门验证的情况下处理javascript:data: URL
  • Angular 的bypassSecurityTrustAs*函数
  • Lit 的unsafeHTML函数
  • Polymer 的inner-h-t-m-l属性和htmlLiteral函数
  • 模板注入
  • 过时的框架插件或组件
  • 等等

当你使用现代 Web 框架时,你需要了解你的框架如何预防 XSS 以及它存在哪些漏洞。有时你需要在框架提供的保护之外进行操作,这意味着输出编码和 HTML 清理可能至关重要。OWASP 将为 React、Vue 和 Angular 提供针对特定框架的速查表。

XSS 防御理念

为了使 XSS 攻击成功,攻击者必须能够在网页中插入并执行恶意内容。因此,Web 应用程序中的所有变量都需要受到保护。确保所有变量都经过验证,然后进行转义或清理,这被称为完美的注入抵抗。任何未经过此过程的变量都是潜在的弱点。框架使确保变量得到正确验证、转义或清理变得容易。

然而,没有哪个框架是完美的,像 React 和 Angular 这样的流行框架中仍然存在安全漏洞。输出编码和 HTML 清理有助于解决这些漏洞。

输出编码

当需要安全地显示用户输入的数据时,建议使用输出编码。变量不应被解释为代码而不是文本。本节涵盖了每种形式的输出编码、其使用位置以及何时根本不应使用动态变量。

首先,当您希望按用户输入的方式显示数据时,请从框架的默认输出编码保护开始。自动编码和转义功能内置于大多数框架中。

如果您没有使用框架或需要弥补框架中的不足,那么您应该使用输出编码库。用户界面中使用的每个变量都应通过输出编码函数进行处理。附录中包含了输出编码库的列表。

存在许多不同的输出编码方法,因为浏览器对 HTML、JS、URL 和 CSS 的解析方式不同。使用错误的编码方法可能会引入弱点或损害应用程序的功能。

“HTML 上下文”的输出编码

“HTML 上下文”指的是在两个基本 HTML 标签之间插入变量,例如<div><b>。例如:

<div> $varUnsafe </div>

攻击者可以修改渲染为$varUnsafe的数据。这可能导致攻击被添加到网页中。例如:

<div> <script>alert`1`</script> </div> // Example Attack

为了将变量安全地添加到 Web 模板的 HTML 上下文中,请对该变量使用 HTML 实体编码。

以下是特定字符的编码值示例:

如果您正在使用 JavaScript 写入 HTML,请查看.textContent属性。它是一个安全接收点,并将自动进行 HTML 实体编码。

&    &amp;
<    &lt;
>    &gt;
"    &quot;
'    &#x27;

“HTML 属性上下文”的输出编码

当变量被放置在 HTML 属性值中时,就会发生“HTML 属性上下文”。您可能希望这样做来更改超链接、隐藏元素、为图像添加 alt 文本或更改内联 CSS 样式。您应该对放置在大多数 HTML 属性中的变量应用 HTML 属性编码。安全 HTML 属性列表在安全接收点部分提供。

<div attr="$varUnsafe">
<div attr=”*x” onblur=”alert(1)*”> // Example Attack

使用引号(如"')来包围您的变量至关重要。引用使得更改变量操作的上下文变得困难,这有助于防止 XSS。引用还显著减少了您需要编码的字符集,使您的应用程序更可靠,编码更容易实现。

如果您正在使用 JavaScript 写入 HTML 属性,请查看.setAttribute[attribute]方法,因为它们将自动进行 HTML 属性编码。只要属性名称是硬编码且无害的(如idclass),它们就是安全接收点。通常,接受 JavaScript 的属性,例如onClick,与不受信任的属性值一起使用时是不安全的。

“JavaScript 上下文”的输出编码

“JavaScript 上下文”指的是变量被放入内联 JavaScript 并嵌入 HTML 文档中的情况。这种情况在大量使用嵌入网页中的自定义 JavaScript 的程序中很常见。

然而,在 JavaScript 中放置变量的唯一“安全”位置是在“带引号的数据值”内。所有其他上下文都是不安全的,您不应在其中放置变量数据。

“带引号数据值”的示例:

<script>alert('$varUnsafe)</script>
<script>x=$varUnsafe</script>
<div onmouseover="'$varUnsafe'"</div>

使用\xHH格式编码所有字符。编码库通常有EncodeForJavaScript或类似的功能来支持此功能。

请查看OWASP Java 编码器 JavaScript 编码示例,了解需要最少编码的正确 JavaScript 使用示例。

对于 JSON,请验证Content-Type头部是application/json而不是text/html,以防止 XSS。

“CSS 上下文”的输出编码

“CSS 上下文”指的是变量被放入内联 CSS 中,这在开发人员希望用户自定义网页外观时很常见。由于 CSS 具有惊人的强大功能,它已被用于多种类型的攻击。变量只应放置在 CSS 属性值中。其他“CSS 上下文”是不安全的,您不应在其中放置变量数据。

<style> selector { property : $varUnsafe; } </style>
<style> selector { property : "$varUnsafe"; } </style>
<span style="property : $varUnsafe">Oh no</span>

如果您正在使用 JavaScript 更改 CSS 属性,请考虑使用style.property = x。这是一个安全接收点,并将自动对其中的数据进行 CSS 编码。

在将变量插入 CSS 属性时,请确保数据经过正确编码和清理,以防止注入攻击。避免将变量直接放入选择器或其他 CSS 上下文。

“URL 上下文”的输出编码

“URL 上下文”指的是变量被放入 URL 中。最常见的是,开发人员会将参数或 URL 片段添加到 URL 基础中,然后将其显示或用于某些操作。对于这些场景,请使用 URL 编码。

<a href="http://www.owasp.org?test=$varUnsafe">link</a >

使用%HH编码格式编码所有字符。确保所有属性都完全加引号,与 JS 和 CSS 相同。

常见错误

在某些情况下,你会在不同的上下文中使用 URL。最常见的情况是将其添加到<a>标签的hrefsrc属性中。在这种情况下,你应该先进行 URL 编码,然后进行 HTML 属性编码。

url = "https://site.com?data=" + urlencode(parameter)
<a href='attributeEncode(url)'>link</a>

如果您正在使用 JavaScript 构造 URL 查询值,请考虑使用window.encodeURIComponent(x)。这是一个安全接收点,并将自动对其中的数据进行 URL 编码。

危险上下文

输出编码并非完美无缺。它并不总是能阻止 XSS。这些位置被称为危险上下文。危险上下文包括:

<script>Directly in a script</script>
<!-- Inside an HTML comment -->
<style>Directly in CSS</style>
<div ToDefineAnAttribute=test />
<ToDefineATag href="/test" />

其他需要注意的区域包括:

  • 回调函数
  • 代码中处理 URL 的位置,例如此 CSS { background-url : “javascript:alert(xss)”; }
  • 所有 JavaScript 事件处理程序(onclick(), onerror(), onmouseover())。
  • 不安全的 JS 函数,如eval(), setInterval(), setTimeout()

不要将变量放入危险上下文,即使进行输出编码,也无法完全阻止 XSS 攻击。

HTML 清理

当用户需要编写 HTML 时,开发人员可能会允许用户在所见即所得编辑器中更改内容的样式或结构。在这种情况下,输出编码将阻止 XSS,但会破坏应用程序的预期功能。样式将无法渲染。在这种情况下,应使用 HTML 清理。

HTML 清理将从变量中去除危险的 HTML,并返回一个安全的 HTML 字符串。OWASP 推荐使用DOMPurify进行 HTML 清理。

let clean = DOMPurify.sanitize(dirty);

还有一些其他事项需要考虑:

  • 如果您清理内容后又对其进行修改,您很容易就会使您的安全工作失效。
  • 如果您清理内容后将其发送给某个库使用,请检查它是否以某种方式更改了该字符串。否则,同样地,您的安全努力也会失效。
  • 您必须定期修补您使用的 DOMPurify 或其他 HTML 清理库。浏览器功能会不断变化,并且绕过方式也经常被发现。

安全接收点

安全专业人员通常使用源和接收点来讨论。如果你污染了一条河流,它会流向下游的某个地方。计算机安全也是如此。XSS 接收点是变量被放置到你的网页中的位置。

值得庆幸的是,许多可以放置变量的接收点是安全的。这是因为这些接收点将变量视为文本,并且永远不会执行它。尝试重构你的代码,移除对不安全接收点的引用,如 innerHTML,而是使用 textContent 或 value。

elem.textContent = dangerVariable;
elem.insertAdjacentText(dangerVariable);
elem.className = dangerVariable;
elem.setAttribute(safeName, dangerVariable);
formfield.value = dangerVariable;
document.createTextNode(dangerVariable);
document.createElement(dangerVariable);
elem.innerHTML = DOMPurify.sanitize(dangerVar);

安全的 HTML 属性包括:align, alink, alt, bgcolor, border, cellpadding, cellspacing, class, color, cols, colspan, coords, dir, face, height, hspace, ismap, lang, marginheight, marginwidth, multiple, nohref, noresize, noshade, nowrap, ref, rel, rev, rows, rowspan, scrolling, shape, span, summary, tabindex, title, usemap, valign, value, vlink, vspace, width

对于未在此处列出的属性,请确保如果提供了 JavaScript 代码作为值,则不能执行它。

其他控制措施

框架安全保护、输出编码和 HTML 清理将为您的应用程序提供最佳保护。OWASP 在所有情况下都推荐这些措施。

除了上述措施外,请考虑采用以下控制措施。

  • Cookie 属性 - 这些属性改变了 JavaScript 和浏览器与 Cookie 的交互方式。Cookie 属性试图限制 XSS 攻击的影响,但不能阻止恶意内容的执行或解决漏洞的根本原因。
  • 内容安全策略(CSP)- 一个阻止内容加载的允许列表。实现时很容易出错,因此不应将其作为主要的防御机制。将 CSP 作为额外的防御层,并查看此处的速查表
  • Web 应用防火墙(WAFs)- 它们查找已知的攻击字符串并阻止它们。WAF 不可靠,新的绕过技术不断被发现。WAF 也无法解决 XSS 漏洞的根本原因。此外,WAF 还遗漏了一类完全在客户端运行的 XSS 漏洞。不建议使用 WAF 来阻止 XSS,尤其是基于 DOM 的 XSS。

XSS 预防规则摘要

这些 HTML 片段展示了如何在各种不同上下文中安全地渲染不可信数据。

数据类型:字符串 上下文:HTML 正文 代码:<span>不可信数据</span> 示例防御:HTML 实体编码(规则 #1)

数据类型:字符串 上下文:安全 HTML 属性 代码:<input type="text" name="fname" value="不可信数据 "> 示例防御:激进的 HTML 实体编码(规则 #2),仅将不可信数据放入安全属性列表(如下所列),严格验证不安全属性如 background, ID 和 name。

数据类型:字符串 上下文:GET 参数 代码:<a href="/site/search?value=不可信数据 ">点击我</a> 示例防御:URL 编码(规则 #5)。

数据类型:字符串 上下文:SRC 或 HREF 属性中的不可信 URL 代码:<a href="不可信 URL ">点击我</a> <iframe src="不可信 URL " /> 示例防御:规范化输入,URL 验证,安全 URL 验证,只允许 http 和 HTTPS URL(避免使用 JavaScript 协议打开新窗口),属性编码器。

数据类型:字符串 上下文:CSS 值 代码:HTML <div style="width: 不可信数据 ;">选择</div> 示例防御:严格结构验证(规则 #4),CSS 十六进制编码,良好设计的 CSS 功能。

数据类型:字符串 上下文:JavaScript 变量 代码:<script>var currentValue='不可信数据 ';</script> <script>someFunction('不可信数据 ');</script> 示例防御:确保 JavaScript 变量被引用,JavaScript 十六进制编码,JavaScript Unicode 编码,避免反斜杠编码(\"\'\\)。

数据类型:HTML 上下文:HTML 正文 代码:<div>不可信 HTML</div> 示例防御:HTML 验证(JSoup, AntiSamy, HTML Sanitizer...)。

数据类型:字符串 上下文:DOM XSS 代码:<script>document.write("不可信输入: " + document.location.hash );<script/> 示例防御:基于 DOM 的 XSS 预防速查表 |

输出编码规则摘要

输出编码(与跨站脚本攻击相关)的目的是将不可信输入转换为安全形式,以便输入在浏览器中作为数据显示给用户,而不会作为代码执行。下表列出了阻止跨站脚本攻击所需的关键输出编码方法。

编码类型:HTML 实体编码 编码机制:将&转换为&amp;,将<转换为&lt;,将>转换为&gt;,将"转换为&quot;,将'转换为&#x27;

编码类型:HTML 属性编码 编码机制:使用 HTML 实体&#xHH;格式编码所有字符,包括空格,其中HH表示字符在 Unicode 中的十六进制值。例如,A变为&#x41;。所有字母数字字符(字母 A 到 Z、a 到 z,以及数字 0 到 9)保持不编码。

编码类型:URL 编码 编码机制:使用 W3C 规范中规定的标准百分比编码,对参数值进行编码。请谨慎,只对参数值进行编码,而不是对整个 URL 或 URL 的路径片段进行编码。

编码类型:JavaScript 编码 编码机制:使用 Unicode \uXXXX编码格式编码所有字符,其中XXXX表示十六进制 Unicode 码点。例如,A变为\u0041。所有字母数字字符(字母 A 到 Z、a 到 z,以及数字 0 到 9)保持不编码。

编码类型:CSS 十六进制编码 编码机制:CSS 编码支持\XX\XXXXXX两种格式。为确保正确编码,请考虑以下选项:(a) 在 CSS 编码后添加一个空格(CSS 解析器将忽略该空格),或 (b) 使用六个字符的完整 CSS 编码格式,通过零填充值。例如,A变为\41(短格式)或\000041(完整格式)。字母数字字符(字母 A 到 Z、a 到 z,以及数字 0 到 9)保持不编码。

常见反模式:应避免的无效方法

防御 XSS 是一项艰巨的任务。因此,有些人试图寻找预防 XSS 的捷径。

我们将探讨两种常见的反模式,它们经常出现在旧文章中,但在 Stack Overflow 和其他开发人员交流场所的现代 XSS 防御文章中仍被普遍引用为解决方案。

单独依赖内容安全策略(CSP)头部

首先,让我们明确一点,我们强烈支持在正确使用 CSP 的情况下。在 XSS 防御的上下文中,CSP 在以下情况效果最佳:

  • 作为深度防御技术使用。
  • 为每个单独的应用程序定制,而不是作为一刀切的企业解决方案部署。

我们反对的是针对整个企业采用一揽子 CSP 策略。这种方法的问题在于:

问题 1 - 假设浏览器版本对 CSP 的支持相同

通常存在一个隐式假设,即所有客户浏览器都支持您的全覆盖 CSP 策略所使用的所有 CSP 构造。此外,这种假设通常是在没有明确测试User-Agent请求头以查看其是否确实是受支持的浏览器类型,并且在不受支持时拒绝使用站点的情况下进行的。为什么?因为大多数企业不希望因为客户使用不支持某些 CSP Level 2 或 Level 3 构造的过时浏览器而将他们拒之门外,而这些构造是他们赖以预防 XSS 的。(统计上,几乎所有浏览器都支持 CSP Level 1 指令,所以除非您担心爷爷拿出他那台旧的 Windows 98 笔记本电脑并使用某个古老的 Internet Explorer 版本访问您的网站,否则 CSP Level 1 的支持可能可以假定是存在的。)

问题 2 - 支持旧版应用程序的问题

强制性的全企业范围 CSP 响应头不可避免地会破坏某些 Web 应用程序,尤其是旧版应用程序。这导致业务部门抵制应用程序安全(AppSec)指南,并不可避免地导致 AppSec 发布豁免和/或安全例外,直到应用程序代码能够得到修补。但是,这些安全例外会在您的 XSS 防御中留下裂缝,即使裂缝是暂时的,它们仍然可能影响您的业务,至少在声誉方面。

依赖 HTTP 拦截器

我们观察到的另一个常见反模式是尝试在某种拦截器中处理验证和/或输出编码,例如通常实现org.springframework.web.servlet.HandlerInterceptor的 Spring 拦截器,或实现javax.servlet.Filter的 JavaEE servlet 过滤器。虽然这对于非常特定的应用程序(例如,如果您验证所有渲染的输入请求都只是字母数字数据)可能成功,但它违反了 XSS 防御的主要原则,即尽可能在数据渲染的位置附近执行输出编码。通常,HTTP 请求会检查查询和 POST 参数,但其他可能被渲染的 HTTP 请求头,例如 cookie 数据,则不被检查。我们看到的常见方法是有人会调用ESAPI.validator().getValidSafeHTML()ESAPI.encoder.canonicalize(),并根据结果重定向到错误页面或调用类似ESAPI.encoder().encodeForHTML()的方法。除了这种方法通常会遗漏被污染的输入(例如请求头或 URI 中的“额外路径信息”)之外,这种方法完全忽略了输出编码完全是非上下文的这一事实。例如,servlet 过滤器如何知道输入查询参数将呈现在 HTML 上下文(即,在 HTML 标签之间),而不是在 JavaScript 上下文(例如,在<script>标签内或与 JavaScript 事件处理程序属性一起使用)?它不知道。而且由于 JavaScript 和 HTML 编码不可互换,您仍然容易受到 XSS 攻击。

除非您的过滤器或拦截器完全了解您的应用程序,并特别清楚您的应用程序如何使用给定请求的每个参数,否则它无法成功处理所有可能的边缘情况。我们认为,采用这种方法永远无法做到这一点,因为提供所需的额外上下文过于复杂,并且如果您尝试这样做,几乎不可避免地会意外引入其他漏洞(其影响可能比 XSS 严重得多)。

这种天真的方法通常至少存在以下四个问题之一。

问题 1 - 特定上下文的编码不适用于所有 URI 路径

一个问题是编码不当,这仍然可能允许在您应用程序的某些 URI 路径中发生可利用的 XSS。例如,一个通常显示在 HTML 标签之间的 POST 请求的“lastname”表单参数,其 HTML 编码是足够的,但可能存在一两个边缘情况,其中 lastname 实际上作为 JavaScript 块的一部分被渲染,此时 HTML 编码不足以防御,从而容易受到 XSS 攻击。

问题 2 - 拦截器方法可能导致不正确或双重编码造成的渲染损坏

这种方法的第二个问题是可能导致应用程序出现不正确或双重编码。例如,假设在前面的示例中,开发人员已经对 lastname 的 JavaScript 渲染进行了正确的输出编码。但是,如果它也已经被 HTML 输出编码了,那么在渲染时,一个合法的姓氏,如“O'Hara”,可能会被渲染成“O\'Hara”。

虽然第二种情况并非严格意义上的安全问题,但如果经常发生,可能会导致业务部门抵制使用过滤器,从而业务部门可能会决定禁用过滤器或指定某些页面或参数不被过滤,这反过来会削弱它所提供的任何 XSS 防御。

问题 3 - 拦截器对基于 DOM 的 XSS 无效

第三个问题是它对基于 DOM 的 XSS 无效。要做到这一点,拦截器或过滤器必须扫描作为 HTTP 响应一部分的所有 JavaScript 内容,尝试找出被污染的输出,并查看其是否容易受到基于 DOM 的 XSS 的攻击。这根本不切实际。

问题 4 - 拦截器对响应数据源于应用程序外部的情况无效

拦截器的最后一个问题是,它们通常对应用程序响应中源自其他内部来源(例如内部基于 REST 的 Web 服务甚至内部数据库)的数据一无所知。问题在于,除非您的应用程序在检索数据时严格验证该数据(这通常是您的应用程序具有足够上下文以使用允许列表方法进行严格数据验证的唯一时机),否则该数据应始终被视为被污染。但是,如果您试图在拦截器(例如 Java servlet 过滤器)的 HTTP 响应端对所有被污染的数据进行输出编码或 HTML 清理,此时,您的应用程序的拦截器将不知道是否存在来自那些 REST Web 服务或其他数据库的被污染数据。响应侧拦截器通常用于提供 XSS 防御的方法是,只将被匹配的“输入参数”视为被污染,并对其进行输出编码或 HTML 清理,而其他所有内容都被认为是安全的。但有时并非如此?尽管经常假设所有内部 Web 服务和所有内部数据库都可以“信任”并直接使用,但除非您已将其包含在您的应用程序的深度威胁建模中,否则这是一个非常糟糕的假设。

例如,假设您正在开发一个向客户显示其详细月账单的应用程序。我们假设您的应用程序正在查询一个外部(即不属于您特定应用程序)内部数据库或 REST Web 服务,您的应用程序使用它来获取用户的全名、地址等信息。但该数据源自另一个您假定“受信任”的应用程序,但实际上该应用程序在各种客户地址相关字段上存在一个未报告的持久性 XSS 漏洞。此外,我们假设您公司的客户支持人员可以检查客户的详细账单,以便在客户对账单有疑问时提供帮助。因此,一个恶意客户决定在地址字段中植入一个 XSS 炸弹,然后致电客户服务寻求账单帮助。如果发生这种情况,试图阻止 XSS 的拦截器将完全遗漏这一点,结果将比仅仅弹出一个警报框显示“1”或“XSS”或“pwn'd”更糟。

总结

最后一点:如果部署拦截器/过滤器作为 XSS 防御是一种对抗 XSS 攻击的有效方法,难道您不认为它会被纳入所有商业 Web 应用程序防火墙(WAF)中,并且成为 OWASP 在本速查表中推荐的方法吗?

XSS 攻击速查表

以下文章描述了攻击者如何利用不同类型的 XSS 漏洞(本文旨在帮助您避免这些漏洞):

XSS 漏洞描述

  • OWASP 关于 XSS 漏洞的文章。

关于 XSS 漏洞类型的讨论

如何审查代码以发现跨站脚本漏洞

如何测试跨站脚本漏洞