安全安全 HTML

跨站点脚本 (XSS) 漏洞是一类 Web 应用程序安全漏洞,攻击者可以利用这些漏洞在受害者浏览器会话的上下文中执行任意恶意 JavaScript。反过来,这些恶意脚本可以例如窃取用户的会话凭据(导致劫持和完全破坏用户的会话),从受害者的帐户中提取和泄露敏感或机密数据,或以受害者的名义执行攻击者选择的交易。

在 GWT 应用程序中,Web 应用程序 UI 的许多方面都以抽象(例如小部件)的形式表达,这些抽象不会暴露将不受信任的数据解释为 HTML 标记或脚本的可能性。因此,与基于直接将 UI 渲染为 HTML 的框架构建的应用程序相比,GWT 应用程序天生更不容易受到 XSS 漏洞的影响(例如服务器端模板系统,除了那些自动转义模板变量的模板系统,根据变量出现的 HTML 上下文)。

但是,GWT 应用程序并非天生就能免受 XSS 漏洞的影响。

GWT 应用程序中一大类潜在的 XSS 漏洞源于使用导致浏览器将其参数评估为 HTML 的方法,例如 setInnerHTML(String)setHTML(String) 以及包含 HTML 的小部件(例如 HTML)的构造函数。如果应用程序将字符串传递给此类方法,即使字符串部分来自不受信任的输入,应用程序也会容易受到 XSS 攻击。在这种情况下,不受信任的输入包括直接用户输入,客户端应用程序从服务器接收到的数据,这些数据可能由其他恶意用户提供给服务器,以及从 URL 片段读取的历史记录标记中获取的值。

本文档介绍了一个新的安全包以及配套的编码指南,这些指南可帮助开发人员避免此类 XSS 漏洞,同时最大限度地减少运行时和开发工作量的开销。编码指南的主要目标是促进应用程序代码审查,以高置信度确定是否存在此类 XSS 错误。

请注意,本文档未涵盖 GWT 应用程序可能容易受到的其他类别的 XSS 漏洞,例如服务器端 XSS,以及其他类别的客户端 XSS(例如,在不受信任的字符串上调用 eval() 在本机 JavaScript 中)。

  1. 编码指南
  2. 小部件客户端代码开发人员的编码指南
    1. 优先使用纯文本小部件
    2. 使用 UiBinder 进行声明式布局
    3. 使用 SafeHtml 类型来表示 XSS 安全的 HTML
    4. 创建 SafeHtml 值
  3. 小部件开发人员的编码指南
    1. 提供 SafeHtml 方法和构造函数
    2. 在靠近值的使用处“解开”SafeHtml
  4. 警告和限制

编码指南

这些指南的目标是双重的

  1. 如果 GWT 应用程序的代码库一致且全面地应用了这些指南,则应该不会出现攻击者控制的字符串在浏览器中被评估为 HTML 导致的 XSS 漏洞。
  2. 代码结构应该便于轻松审查是否存在此类 XSS 漏洞 - 对于每个使用可能存在 XSS 漏洞的方法(例如 Element.setInnerHTML),它应该“明显”地表明这种使用不会导致 XSS 漏洞。

第二个目标基于希望对不存在此类错误有高度的信心。在审查代码时,通常很难且容易出错地确定传递给某个方法的值是否可能受到攻击者的控制,尤其是在值是通过一系列分配和方法调用传递时。

因此,这些指南背后的核心思想是使用一种类型来封装在 HTML 上下文中安全使用的字符串,将安全的 HTML 构造为该类型的实例,并使用该类型将字符串“传输”到尽可能靠近使用它们作为 HTML 的代码位置。由于该类型的约定(以及 Java 的类型安全)作为假设,因此此类使用很容易被证明不存在 XSS 漏洞。

小部件客户端代码开发人员的编码指南

以下指南针对使用现有小部件库(尤其是与 GWT 一起分发的核心小部件库)的客户端代码开发人员。

优先使用纯文本小部件

避免 XSS 错误和编写易于识别为无 XSS 漏洞的代码的最佳方法是,除非绝对必要,否则根本不要使用将参数解释为 HTML 的 API 方法和小部件。

例如,在 GWT 应用程序代码中经常会看到以下代码

HTML widget = new HTML("Some text in the widget");

widget.setHTML(someText);

在第一个示例中,很明显,传递给 HTML 构造函数的值不会导致 XSS 漏洞:该值不包含 HTML 标记,此外,该值是编译时常量,因此不可能被攻击者操纵。在第二个示例中,审查者可能会发现,如果变量 someText 在代码中被分配为“nearby”,那么该调用是安全的;但是,如果提供的值通过几个方法调用层作为参数传递,那么这一点就不那么明显了。此外,如果调用代码由不知道该值将在 HTML 上下文中使用的开发人员更改,那么这种情况很可能会导致未来的代码迭代中出现错误。

在这种情况下,建议使用非 HTML 等效项,即 Label 小部件和 setText 方法,它们始终能防御 XSS,即使传递给 Label 构造函数或 setText 方法的字符串在攻击者的控制之下。类似地,在 DOM 元素上使用 setInnerText 而不是 setInnerHTML

使用 UiBinder 进行声明式布局

使用GWT UiBinder 是在 GWT 应用程序中声明式地创建小部件和 DOM 结构的首选方法。除了 UiBinder 的主要优势(代码和布局的清晰分离、性能、内置国际化支持)之外,使用 UiBinder 通常也会导致比使用临时方法(例如,将 HTML 标记组装到要放置在 HTML 小部件中的方法)更容易防御 XSS 漏洞的代码。

通常,UiBinder 声明的布局中的“叶节点”将是其数据内容为纯文本而不是 HTML 标记的小部件。因此,它们自然地通过 setText 而不是 setHTML 或等效方法填充,从而完全避免了 XSS 漏洞的可能性。

使用 SafeHtml 类型来表示 XSS 安全的 HTML

在某些情况下,使用 UiBinder 或类似方法并不实际或过于麻烦。com.google.gwt.safehtml 包提供了一些类型和类,可用于编写此类代码并确保它能防御 XSS。

该包提供了一个接口 com.google.gwt.safehtml.shared.SafeHtml,用于表示在 HTML 上下文中安全使用的字符串子集,这意味着在浏览器中将字符串评估为 HTML 不会导致脚本执行。更具体地说,该接口的所有实现都必须遵守类型约定,即对实例调用 asString() 方法始终返回在上述意义上为 HTML 安全的字符串。此外,该类型的约定要求任何两个 SafeHtml 包装字符串的串联本身必须在 HTML 上下文中安全使用。

随着 com.google.gwt.safehtml 包的引入,所有核心 GWT 库的小部件(这些小部件采用被解释为 HTML 的 String 参数)都已通过采用 SafeHtml 类型值的相应方法进行了增强。特别是,所有实现 com.google.gwt.user.client.ui.HasHTML(或 com.google.gwt.user.client.ui.HasDirectionalHtml)接口的小部件也都实现了 com.google.gwt.safehtml.client.HasSafeHtml(或 com.google.gwt.user.client.ui.HasDirectionalSafeHtml,分别)接口。这些接口定义了

public void setHTML(SafeHtml html);
public void setHTML(SafeHtml html, Direction dir);

作为 setHTML(String)setHTML(String, Direction) 的安全替代方案。

例如,com.google.gwt.user.client.ui.HTML 小部件已通过以下构造函数和方法进行了增强

public class HTML extends Label 
    implements HasDirectionalHtml, HasDirectionalSafeHtml {
  // ...
  public HTML(SafeHtml html);
  public HTML(SafeHtml html, Direction dir);
  @Override
  public void setHTML(SafeHtml html);
  @Override
  public void setHTML(SafeHtml html, Direction dir);
}

这些编码指南的核心是,GWT 应用程序的开发者不应该使用带有 `String` 类型参数的构造函数和方法,这些参数的值被解释为 HTML,而应该使用 `SafeHtml` 等价物。

创建 SafeHtml 值

`safehtml` 包提供了一些工具来创建 `SafeHtml` 实例,涵盖了 GWT 应用程序通常操作包含 HTML 标记的字符串的许多常见场景。

  • 一个 构建器类,通过安全地将开发者控制的 HTML 标记片段与(可能由攻击者控制的)值组合来方便创建 `SafeHtml` 值。
  • 一个 模板机制,允许定义结构化 HTML 标记的片段,这些片段的值在运行时安全地进行插值。
  • I18N `Messages` 可以以 `SafeHtml` 的形式返回本地化的消息。
  • 许多 便利方法 可用于从字符串创建 `SafeHtml` 值。
  • 一个 简单的 HTML 净化器,接受输入中有限的 HTML 标记子集,并将该子集之外的任何 HTML 标记进行 HTML 转义。

上述每种机制都经过仔细审查,以确保符合 `SafeHtml` 类型契约。

SafeHtmlBuilder

在许多场景中,将在 HTML 上下文中使用的字符串部分地从受信任的字符串(例如,在应用程序源代码中定义的 HTML 标记片段)和不受信任的字符串(可能受潜在攻击者控制)连接起来。 SafeHtmlBuilder 类提供了一个构建器接口,支持这种用例,同时确保字符串的不可信部分得到适当的转义。

考虑这个使用示例

public void showItems(List<String> items) {
  SafeHtmlBuilder builder = new SafeHtmlBuilder();
  for (String item : items) {
    builder.appendEscaped(item).appendHtmlConstant("<br/>");
  }
  itemsListHtml.setHTML(builder.toSafeHtml());
}

SafeHtmlBuilder 的 `appendHtmlConstant` 方法用于将 HTML 的常量片段附加到构建器,而不转义参数。相反,`appendEscaped` 方法将在附加之前对它的字符串参数进行 HTML 转义。

为了让 `SafeHtmlBuilder` 遵守 `SafeHtml` 契约,使用它的代码必须反过来遵守以下规则

  1. `appendHtmlConstant` 的参数必须是字符串字面量(或者更普遍地说,必须在编译时完全确定)。
  2. 提供的字符串不能在 HTML 标记内结束。例如,以下用法将是非法的,因为第一个 `appendHtmlConstant` 的参数包含一个不完整的 `<a>` 标记;该字符串在该标记的 `href` 属性值的上下文中结束
builder.appendHtmlConstant("<a href='").appendEscaped(url).appendHtmlConstant("'>")

第一个规则是必要的,以确保传递给 `appendHtmlConstant` 的字符串不可能受攻击者控制。第二个规则是必要的,因为即使在 HTML 标记属性内使用的不可信字符串被 HTML 转义,也可能导致脚本执行。在上面的示例中,如果 `url` 的值为 `javascript:evil_js_code()`,则可能会发生脚本执行。

在托管模式下执行客户端代码,或在启用断言的情况下执行服务器端代码时,`appendHtmlConstant` 会解析它的参数并检查它是否满足第二个约束。出于性能原因,此检查不会在客户端代码的生产模式下执行,也不会在服务器端禁用断言的情况下执行。

SafeHtmlBuilder 还提供 `append(SafeHtml)` 方法。所提供的 `SafeHtml` 的内容将被附加到构建器,而无需事先转义(由于 `SafeHtml` 契约,可以假设它在 HTML 上是安全的)。此方法允许将作为 `SafeHtml` 封装的 HTML 片段组合成更大的 `SafeHtml` 片段。

使用 SafeHtmlTemplates 接口创建 HTML

为了方便创建包含更复杂 HTML 标记的 SafeHtml 实例,`safehtml` 包提供了一种编译时绑定的模板机制,可以像在这个示例中一样使用

public class MyWidget ... {
// ...
  public interface MyTemplates extends SafeHtmlTemplates {
    @Template("<span class=\"{3}\">{0}: <a href=\"{1}\">{2}</a></span>")
    SafeHtml messageWithLink(SafeHtml message, String url, String linkText,
        String style);
  }
 
  private static final MyTemplates TEMPLATES =
      GWT.create(MyTemplates.class);
 
  public void useTemplate(...) {
    SafeHtml message;
    String url;
    String linkText;
    String style;
    // ...
    InlineHTML messageWithLinkInlineHTML = new InlineHTML(
        TEMPLATES.messageWithLink(message, url, linkText, style));
    // ...
  }

使用 `GWT.create()` 实例化 SafeHtmlTemplates 接口会返回一个实现的实例,该实例是在编译时生成的。代码生成器会解析每个模板方法的 `@Template` 注释的值作为 (X)HTML 模板,模板变量由花括号占位符表示,这些占位符通过索引引用相应的模板方法参数。

SafeHtmlTemplates 接口中的所有方法必须具有 `SafeHtml` 的返回类型。此类方法的编译时生成实现被构造为返回实际上遵守 `SafeHtml` 类型契约的 `SafeHtml` 实例。代码生成器通过在生成的代码中结合编译时检查和运行时检查来实现此保证(但是,请参见下面关于当前实现限制的说明)

  • 模板使用宽松的 HTML 流解析器进行解析,该解析器接受类似于网络浏览器通常接受的 HTML。解析器不要求模板包含平衡的 HTML 标记。但是,解析器和模板代码生成器对输入模板强制执行以下约束:模板参数不能出现在 HTML 注释中,参数不能出现在 Javascript 上下文中(例如,在 `<script>` 标记内,或在 `onclick` 处理程序中),HTML 属性中的参数只能出现在值中并且必须在引号中关闭(例如,`<tag attribute="{0}">` 将被允许,`<tag {0} attribute={1}>` 将不被允许),并且模板不能在标记内或属性内结束。例如,以下不是有效的模板
<span><{0} class="xyz" {1}="..."/></span>
  • 生成的代码根据模板参数出现的 HTML 上下文和相应模板方法参数的声明类型,通过适当的净化器和转义方法传递实际的模板参数,如下所述。

限制:解析器的当前实现不能保证具有 CSS 上下文中模板变量的模板的 `SafeHtml` 契约(即,在 `style` 属性或标记内)。当解析器遇到在样式属性或标记中具有变量的模板时(例如,`<div style="{0}">`),将发出警告。建议开发人员仔细审查这些情况,以确保传递给模板的参数来自受信任的来源或已适当地净化。

模板处理

对模板参数应用的转义和/或净化的选择根据以下规则进行

内部 HTML 上下文中的参数

出现在普通内部 HTML 上下文中的参数(例如,示例中的参数 `0` 和 `2`)按如下方式处理

  • 如果相应模板方法参数的声明类型为 `SafeHtml`(例如,示例中的参数 `message`),则参数的实际值将在没有进一步验证或转义的情况下发出。
  • 如果声明类型为 `String`(例如,示例中的参数 `linkText`),则参数的实际值将在运行时进行 HTML 转义,然后再发出。
  • 如果声明类型为基本类型(例如,数字或布尔类型),则该值将转换为 `String` 并发出,但不会通过转义方法,因为基本类型的 String 表示始终没有 HTML 特殊字符。
  • 如果声明类型为任何其他类型,则参数的值将首先转换为 `String`,然后进行 HTML 转义。
属性上下文中的参数

出现在属性上下文中的参数(例如,示例中的 `1` 和 `3`)按如下方式处理

  • 如果相应模板方法参数的声明类型为 `String`,则参数的值将首先转换为 `String`。
  • 然后对参数进行 HTML 转义,然后再发出。

请注意,如果声明类型为 `SafeHtml` 的参数出现在属性上下文中,则不会对其进行特殊处理(即,此类参数不会跳过转义)。这是因为 `SafeHtml` 字符串可以包含未转义的 HTML 特殊字符(只要此类 HTML 标记是安全的);但是属性值的内部不允许出现任何未转义的 HTML 特殊字符。

URI 值属性上下文中的参数

出现在 URI 值属性(如 `src` 或 `href`)开头的参数,例如示例中的参数 `1` 但不是 `3`,将被特殊处理

  • 在进行 HTML 转义之前,将对参数的值进行净化,以确保它可以安全地用作 URI 值 HTML 属性的值。此净化按如下方式执行(参见 UriUtils.sanitizeUri(String)
    • 没有方案的 URI 被认为是安全的,并且按原样使用。
    • 方案等于 `http, https, ftp, mailto` 之一的 URI 被认为是安全的,并且按原样使用。
    • 任何其他 URI 被认为是不安全的,并且被丢弃;相反,将“空”URI“#”插入模板。

限制:没有用于参数语法的转义机制。例如,不可能编写一个模板,该模板导致包含形式为 `0` 的子字符串的字面量输出。

便利方法

SafeHtmlUtils 类提供了一些便利方法,用于从字符串创建 `SafeHtml` 值

  • SafeHtmlUtils.fromString(String s) 对它的参数进行 HTML 转义,并将结果作为 `SafeHtml` 封装返回。
  • SafeHtmlUtils.fromSafeConstant(String s) 返回作为 `SafeHtml` 封装的编译时常量字符串,而不转义该值。为了让 `fromSafeConstant` 遵守 `SafeHtml` 契约,使用它的代码必须反过来遵守与 `SafeHtmlBuilder.appendHtmlConstant` 相同的约束
    1. `fromSafeConstant` 的参数必须是字符串字面量(或者更普遍地说,必须在编译时完全确定)。
    2. 提供的字符串不能在 HTML 标记内结束。例如,以下用法将是非法的,因为传递给 `fromSafeConstant` 的值包含一个不完整的 `<a>` 标记;该字符串在该标记的 `href` 属性值的上下文中结束
SafeHtml safeHtml = SafeHtmlUtils.fromSafeConstant("<a href='");
  • SafeHtmlUtils.fromTrustedString(Strings)

    将它的参数作为 `SafeHtml` 返回,而不执行任何形式的验证或转义。开发人员有责任确保传递给此方法的值符合 `SafeHtml` 契约。

    强烈建议不要使用此方法;它仅用于在以下情况下使用:现有代码生成满足 `SafeHtml` 类型契约的值,但此类代码不能轻松地重构为自身生成 `SafeHtml` 类型的值。

SimpleHtmlSanitizer

SimpleHtmlSanitizer 通过在运行时应用简单的净化算法,从输入字符串生成 `SafeHtml` 实例。

它适用于以下情况:代码接收包含简单 HTML 标记的字符串,例如,来自服务器端后端。一个例子可能是带有 `<b>` 标记标记查询词的搜索片段,例如“`<b>Flowers</b>, roses, plants &amp; gift baskets delivered. Order <b>flowers</b> from ..."”。

需要呈现此类字符串的 GWT 应用程序不能简单地对它们进行 HTML 转义,因为它们确实包含合法的 HTML。同时,应用程序的开发者可能不希望依赖生成这些片段的后端完全没有错误,并且永远不会生成可能包含第三方控制和潜在恶意 HTML 标记的字符串。相反,此类字符串可以通过将它们传递给 `SimpleHtmlSanitizer` 来作为 `SafeHtml` 封装,例如

SafeHtml snippetHtml = SimpleHtmlSanitizer.sanitizeHtml(snippet);

SimpleHtmlSanitizer 使用简单的净化算法,该算法接受以下标记

  • 一个基本 HTML 标签的白名单,不包含属性,包括<b>,<em>, <i>, <h1>, ..., <h5>, <hr>, <ul>,<ol>, <li>以及相应的结束标签。
  • HTML 实体和实体引用,例如&#39;, &#x2F;,&amp;, &quot;,等等。

此子集中的 HTML 标记不会被转义;不在上述集合中的子字符串中的 HTML 元字符将被转义。

例如,字符串

foo < bar &amp; that is <em>good</em>, <span style="foo: bar">...

将被清理为

foo &amp;lt; bar &amp;amp; that is <em>good</em>, &amp;lt;span style=&amp;quot;foo: bar&amp;quot;&amp;gt;...

请注意,SimpleHtmlSanitizer 不保证生成的 HTML 是格式良好的,例如,HTML 中所有剩余的标签都是平衡的。

清理结果将作为 SafeHtml 返回,可以附加到 SafeHtmlBuilder 而不进行转义。

小部件开发人员的编码指南

小部件(或其他库组件)的开发人员应考虑正在开发的小部件的 HTML 安全性。如果可能,应设计和实现小部件,使其在任何情况下都不会导致 XSS 漏洞。如果这样做不可行,则应设计和实现小部件,使其对于客户端代码的开发人员来说使用起来直观自然,并且易于安全使用,此外,还应使其便于代码审查人员确定小部件的特定使用是否安全。

例如,给定实例的 GWT 的 HTML 小部件不会导致 XSS 漏洞,只要其使用不涉及调用 HTML(String) 和相关的构造函数,或 HTML.setHTML(String)HTML.setHTML(String, Direction) 方法。使用等效 SafeHtml 构造函数和方法的代码始终是安全的。

提供 SafeHtml 方法和构造函数

具有构造函数或方法(其参数被解释为 HTML)的小部件应提供等效的构造函数和方法,这些构造函数和方法采用 SafeHtml 值。特别是,实现 HasHTMLHasDirectionalHtml 的小部件也应分别实现 HasSafeHtmlHasDirectionalSafeHtml

在接近值使用的地方“解包”SafeHtml

包装在 SafeHtml 中的值最终将在 HTML 上下文中使用,例如,用于设置 DOM 元素的 innerHTML

为了使小部件实现尽可能“明显安全”,应尽可能靠近这种使用来提取 SafeHtmlString 内容。例如,SafeHtml 值应在将其分配给 innerHTML 之前立即解包,而不是更早。

element.setInnerHTML(safeHtml.asString());

由其他小部件组成的小部件在初始化子小部件时不应解包 SafeHtml 值,而是将 SafeHtml 传递给子小部件。例如,编写

public class MyPanel extends HorizontalPanel {
  InlineHTML messageWidget;
  SafeHtml currentMessage;

  public void setMessage(SafeHtml newMessage) {
    currentMessage = newMessage;
    updateUi();
  }

  private void updateUi() {
    messageWidget.setHTML(currentMessage);
  }
}

而不是

public class MyPanel extends HorizontalPanel {
  InlineHTML messageWidget;
  String currentMessage;

  public void setMessage(SafeHtml newMessage) {
    currentMessage = newMessage.asString();
    updateUi();
  }

  private void updateUi() {
    // Potentially unsafe call to setHTML(String)
    messageWidget.setHTML(currentMessage);
  }
}

虽然这两种实现都提供了安全的外部接口,但第二种实现不像第一种实现那样明显地没有 XSS 漏洞:它涉及对 setHTML(String) 的调用,该调用本身并不安全。传递给 setHTML(String) 的值是从一个字段获取的。为了确定此小部件是否没有 XSS 漏洞,代码审查人员必须检查对该字段的每次赋值,并验证它只能分配安全的 HTML。在上面这样的简单示例中,这非常简单,但在更复杂、真实的代码中,这可能是一个耗时且容易出错的过程。

注意事项和限制

在 GWT 应用程序开发过程中全面一致地使用 SafeHtml 可以大幅减少该应用程序中 XSS 漏洞的发生率。但是,这并不能保证没有 XSS 漏洞。

此类应用程序中可能存在 XSS 漏洞的原因有很多,包括以下原因

  • 创建 SafeHtml 值的代码中可能存在错误,导致它有时会生成违反类型契约的值。如果此类值用作 HTML(例如,分配给 DOM 元素的 innerHTML 属性),则可能存在 XSS 漏洞。
  • 应用程序代码可能错误地使用 SafeHtmlBuilder.appendHtmlConstantSafeHtmlUtils.fromSafeConstant。例如,如果向其中一个方法传递的值不是按要求进行程序控制,而是来自外部输入,则可能存在 XSS 漏洞。
  • 应用程序代码可能错误地使用 SafeHtmlUtils.fromTrustedString。如果此方法用于 SafeHtml 包装基于第三方输入的值,并且不能严格保证遵守 SafeHtml 类型契约,则可能会出现 XSS 漏洞。
  • 应用程序代码可能存在与在 HTML 上下文中使用外部输入无关的 XSS 错误,这超出了 SafeHtml 库和指南的范围。