ASP.NET 安全

保护您的 ASP.NET 应用程序

Adam Tuliper

在上一期中,我讨论了构建 Web 应用程序安全性的重要性,并介绍了包括 SQL 注入和参数篡改在内的一些攻击类型,以及如何防范这些类型的攻击 (msdn.microsoft.com/magazine/hh580736)。 在本文中,我将深入探讨以下两种更常见的攻击,以帮助完善我们的应用程序保护体系:跨站点脚本 (XSS) 和跨站点请求伪造 (CSRF)。

您可能很想知道: 只使用生产安全扫描程序不就行了吗? 扫描程序是查找容易实现的目标的绝佳工具,它们尤其适合查找应用程序和系统配置问题,但它们无法像您一样了解您的应用程序。 因此,您必须熟悉潜在的安全问题,花点时间检查应用程序并为您的软件开发生命周期构建安全性。

跨站点脚本

简介 XSS 攻击是指将脚本恶意注入用户的浏览会话,这通常在用户不知情的情况下发生。 因为一些事故的发生,许多人已经非常熟悉这些类型的攻击,在这些事故中,某大型社交网站受到攻击,其用户发布了他们未授权的消息。 如果攻击者发布恶意脚本并且他可以让浏览器执行该脚本,则该脚本将在受害者会话的上下文中执行,这基本上使攻击者能够对 DOM 执行其所需的任何操作,包括显示伪造的登录对话框或盗取 Cookie。 此类攻击甚至可以在当前页面上安装 HTML 键记录器,以便持续将来自该窗口的输入内容发送到远程站点。

参数篡改的使用方式 攻击者通过多种方法利用 XSS,所有这些方法都需要有未封装或封装不当的输出。 下面我们以需要向最终用户显示简单的状态消息的应用程序为例。 通常,该消息通过查询字符串传递,如图 1 所示。

Query String Message
图 1 查询字符串消息

该方法通常在重定向之后使用以便向用户显示某种状态,例如图 1 中的“Profile Saved”(配置文件已保存)消息。 该消息从查询字符串读取并输出到页面中。 如果输出内容未经过 HTML 编码,任何人都可以轻松地注入 JavaScript 来代替状态消息。 此类攻击被视为反射 XSS 攻击,因为无论查询字符串中有什么内容,它都会呈现在页面中。 在持续性攻击中,恶意脚本通常存储在数据库或 Cookie 中。

图 1 中,您可以看到此 URI 使用了一个 msg 参数。 此 URI 的网页将包含如下代码以便将变量写入页面而不进行编码:

<div class="messages"> <%=Request.QueryString["msg"]%></div>

如果将“Profile Saved”替换为图 2 中显示的脚本,浏览器中将会弹出查询字符串中包含的脚本的警报功能。 此处的漏洞利用的关键在于结果没有经过 HTML 编码,因此浏览器实际上将 <script> 标记解析为有效 JavaScript 并执行。 这显然不是开发人员的初衷。

Injecting Script into the URI
图 2 将脚本注入 URI 中

那就弹出警报吧—有什么大不了的? 下面我们进一步看看该示例,如图 3 所示。 请注意,为了便于说明,我已经缩短了此处的攻击;此语法并不完全正确,但些许的修改会使其成为真正的攻击。

Creating a Malicious Attack
图 3 创建恶意攻击

类似这样的攻击可能导致向用户显示伪造的登录对话框,用户会轻易地在其中输入他们的凭据。 在本例中,将会下载一个远程脚本,默认浏览器安全设置通常允许这种行为。 理论上,只要可以将尚未编码或净化的字符串回显给用户,就可能会发生此类攻击。

图 4 显示了在开发站点上使用类似脚本的攻击,该站点允许用户留下有关其产品的意见。 不过,有些人不是留下真正的产品评论,而是输入了恶意 JavaScript 作为意见。 该脚本现在将向访问该网页的每个用户显示一个登录对话框,收集凭据然后将凭据发送至远程站点。 这是一种持续性 XSS 攻击;脚本存储在数据库中,并对每个访问该页面的人重复执行。

A Persistent XSS Attack Showing a Fake Dialog
图 4 显示伪造对话框的持续性 XSS 攻击

攻击者利用 XSS 的另一方法是通过使用 HTML 元素,例如 HTML 标记中允许使用动态文本时,如下所示:

<img onmouseover=alert([user supplied text])>

如果攻击者注入“onmouseout=alert(docu­ment.cookie)”之类的文本,这将在访问 Cookie 的浏览器中创建以下标记:

<img onmouseover=alert(1) onmouseout=alert(document.cookie) >

没有用于潜在地筛选输入内容的“<script>”标记,也没有需要封装的内容,但这是可以读取 Cookie(可能是一个身份验证 Cookie)的完全有效的 JavaScript。 可根据具体情况采取具体措施提高安全性,但由于存在风险,最好禁止任何用户输入访问此处的这些内嵌代码。

防范 XSS 的方法 严格遵守以下规则将有助于防范应用程序中的大多数(即使不是全部)XSS 攻击:

  1. 确保所有输出内容都经过 HTML 编码。

  2. 禁止用户提供的文本进入任何 HTML 元素属性字符串。

  3. 根据 msdn.microsoft.com/library/3yekbd5b 中的概述,检查 Request.Browser,以阻止应用程序使用 Internet Explorer 6。

  4. 了解您的控件的行为以及其输出是否经过 HTML 编码。 如果未经过 HTML 编码,则对进入控件的数据进行编码。

  5. 使用 Microsoft 防跨站点脚本库 (AntiXSS) 并将其设置为您的默认 HTML 编码器。

  6. 在将 HTML 数据保存到数据库之前,使用 AntiXSS Sanitizer 对象(该库是一个单独的下载文件,将在下文中介绍)调用 GetSafeHtml 或 GetSafeHtmlFragment;不要在保存数据之前对数据进行编码。

  7. 对于 Web 窗体,不要在网页中设置 EnableRequestValidation=false。 遗憾的是,Web 上的大多数用户组文章都建议在出现错误时禁用该设置。 该设置的存在是有原因的,例如,如果向服务器发送回“<X”之类的字符组合,该设置将阻止请求。 如果您的控件将 HTML 发送回服务器并收到图 5 所示的错误,那么理想情况下,您应该在将数据发布到服务器之前对数据进行编码。 这是 WYSIWYG 控件的常见情形,现今的大多数版本都会在将其 HTML 数据发布回服务器之前对该数据进行正确编码。

    Server Error from Unencoded HTML
    图 5 未编码的 HTML 返回的服务器错误

  8. 对于 ASP.NET MVC 3 应用程序,当您需要将 HTML 发布回模型时,不要使用 ValidateInput(false) 来关闭请求验证。 只需向模型属性中添加 [AllowHtml] 即可,如下所示:

public class BlogEntry
{
  public int UserId {get;set;}
  [AllowHtml]
  public string BlogText {get;set;}
}

有些产品会尝试检测字符串中的 <script> 和其他字词组合或正则表达式模式,以试图检测 XSS。 这些产品可提供额外的检查但并不完全可靠,因为攻击者创建了许多变体。 看看 ha.ckers.org/xss.html 中的 XSS 速查表,了解一下检测的困难程度。

为了理解修复方法,假设攻击者通过查询字符串或此处所示的窗体字段注入了一些脚本,这些脚本最终进入我们应用程序的变量中:

string message = Request.QueryString["msg"];

或者:

string message = txtMessage.Text;

请注意,尽管 TextBox 控件对其输出内容进行 HTML 编码,但当您从代码中读取其 Text 属性时,它不会对该属性进行编码。 无论采用上面哪行代码,消息变量中都会得到以下字符串:

message = "<script>alert('bip')</script>"

在包含类似如下代码的网页中,只是因为将以下文本写入页面,JavaScript 就将在用户的浏览器中执行:

<%=message %>

对输出内容进行 HTML 编码会终止此类攻击。 图6 显示了用于对危险数据进行编码的主要选项。

这些选项可防范示例中所示的攻击类型,应在您的应用程序中使用这些选项。

图 6 HTML 编码选项

ASP.NET(MVC 或 Web 窗体) <%=Server.HtmlEncode(message) %>
Web 窗体(ASP.NET 4 语法) <%: message %>
ASP.NET MVC 3 Razor @message
数据绑定

遗憾的是,数据绑定语法并不包含内置的编码语法;在下一版本的 ASP.NET 中,它将以 <%#: %> 形式出现。 到那时,使用:

<%# Server.HtmlEncode(Eval("PropertyName")) %>

更好地进行编码

从 Microsoft.Security.Application 命名空间中的 AntiXSS 库:

Encoder.HtmlEncode(message)

必须了解您的控件,这一点很重要。 哪些控件对您的数据进行 HTML 编码?哪些控件不进行编码? 例如,TextBox 控件会对呈现的输出内容进行 HTML 编码,而 LiteralControl 则不进行编码。 这是一个重要的区别。 分配有以下内容的文本框:

yourTextBoxControl.Text = "Test <script>alert('bip')</script>";

正确地通过以下形式将文本呈现给页面:

Test &lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;

与此相反:

yourLiteralControl.Text = "Test <script>alert('bip')</script>";

导致页面上显示一个 JavaScript 警报,从而确认 XSS 漏洞。 修复代码非常简单:

yourLiteralControl.Text = Server.HtmlEncode(
    "Test <script>alert('bip')</script>");

在 Web 窗体中使用数据绑定时稍微复杂一些。 请看以下示例:

<asp:Repeater ID="Repeater1" runat="server">
    <ItemTemplate>
      <asp:TextBox ID="txtYourField" Text='<%# Bind("YourField") %>'
        runat="server"></asp:TextBox>
    </ItemTemplate>
  </asp:Repeater>

这是否存在漏洞? 不,不存在。 尽管内嵌代码看起来好像能够输出脚本或中断控件引用,但它确实经过编码。

出现这种情况该怎么办:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%# Eval("YourField") %>
  </ItemTemplate>
</asp:Repeater>

它是否存在漏洞? 是的,确实如此。 数据绑定语法 <%# %> 不执行 HTML 编码。 这是修复代码:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Eval("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

请注意,如果在该方案中使用 Bind,则由于 Bind 在后台作为两个单独的调用进行编译的方式不同,因此您将无法打包 Server.HtmlEncode。 这将失败:

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Bind("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

如果使用 Bind 并且不将文本分配给 HTML 编码的控件(如 TextBox 控件),可考虑改用 Eval,以便可以像在上例中那样,将调用打包到 Server.HtmlEncode 中。

ASP.NET MVC 中不存在这种数据绑定概念,因此您需要知道 HTML 帮助程序是否进行编码。 标签和文本框的帮助程序会执行 HTML 编码。 例如,下面的代码:

@Html.TextBox("customerName", "<script>alert('bip')</script>")
@Html.Label("<script>alert('bip')</script>")

呈现为:

<input id="customerName" name="customerName" type="text"
  value="&lt;script>alert(&#39;bip&#39;)&lt;/script>" />
<label for="">&lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;</label>

我在前面提到过 AntiXSS。 AntiXSS 库目前处于版本 4.1 Beta 1 阶段,它经历了一次非常棒的重新编写过程,并且就安全性而言,它提供了比 ASP.NET 附带的编码器更好的 HTML 编码器。 并不是说 Server.HtmlEncode 有什么问题,只是它侧重于兼容性而不是安全性。 AntiXSS 使用不同的方法进行编码。 有关详细信息,请访问 msdn.microsoft.com/security/aa973814

可从 bit.ly/gMcB5K 处获得 Beta 版。 检查 AntiXSS 的 Beta 阶段是否已结束。 如果没有,您需要下载代码并进行编译。 Jon Galloway 在 bit.ly/lGpKWX 中发布了有关此内容的精彩文章。

若要使用 AntiXSS 编码器,您只需进行以下调用即可:

<%@ Import Namespace="Microsoft.Security.Application" %>
...
...
<%= Encoder.HtmlEncode(plainText)%>

ASP.NET MVC 4 添加了一个非常不错的新功能,它允许您替代默认 ASP HTML 编码器,并且您可以使用 AntiXSS 编码器来代替。 在撰写本文时,您需要版本 4.1;因为它当前处于 Beta 阶段,您必须下载代码,进行编译,然后将库作为引用添加到应用程序中—总共需要五分钟时间。 然后,在您的 web.config 中,向 <system.web> 部分添加下面一行:

<httpRuntime encoderType=
  "Microsoft.Security.Application.AntiXssEncoder, AntiXssLibrary"/>

现在,通过图 6 中列出的任意语法(包括 ASP.NET MVC 3 Razor 语法)进行的任何 HTML 编码调用都将通过 AntiXSS 库进行编码。 可插入功能的情况如何?

该库还包括一个可用于在将 HTML 存储到数据库之前清理 HTML 的 Sanitizer 对象,如果向用户提供用于编辑 HTML 的 WYSIWYG 编辑器,则该对象将非常有用。 该调用会尝试将脚本从字符串中删除:

using Microsoft.Security.Application;
...
...
string wysiwygData = "before <script>alert('bip ')</script> after ";
string cleanData = Sanitizer.GetSafeHtmlFragment(wysiwygData);
This results in the following cleaned string that can then be saved to the database:
cleanData = "before  after ";

跨站点请求伪造 (CSRF)

简介 跨站点请求伪造,即 CSRF(读作 sea-surf),是指有人利用您的浏览器和网站之间的信任关系使用无辜用户的会话执行命令时发生的攻击。 如果不了解详情,很难想象这种攻击是如何发生的,因此我们要详细介绍一下。

参数篡改的使用方式 假设小王以 PureShoppingHeaven 网站管理员的身份通过验证。 PureShoppingHeaven 有一个 URL 仅限管理员访问并允许在 URL 中传递信息以执行操作,例如创建新用户,如图 7 所示。

Passing Information on the URL
图 7 在 URL 中传递信息

如果攻击者可以通过任何方法让小王请求此 URL,攻击者的浏览器将从服务器请求此 URL 并发送小王的浏览器中可能已经缓存或正在使用的任何身份验证信息(例如身份验证 Cookie 或其他身份验证令牌,包括 Windows 身份验证)。

这是一个简单的示例,但 CSRF 攻击可能比这复杂得多,除 GET 请求外,还可以纳入窗体 POST,并且可以同时利用 XSS 之类的其他攻击。

假设小王访问一个有漏洞并且漏洞已经被利用的社交网站。 或许攻击者通过 XSS 漏洞在页面上放置了一些 JavaScript,这些 JavaScript 现在使用小王的会话请求 AddUser.aspx URL。 小王访问网页后来自 Fiddler (fiddler2.com) 的以下转储表明,浏览器还发送一个自定义的网站身份验证 Cookie:

GET http://pureshoppingheaven/AddUser.aspx?userName=hacked&pwd=secret HTTP/1.1
Host: pureshoppingheaven
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
Cookie: CUSTOMERAUTHCOOKIE=a465bc0b-e1e2-4052-8292-484d884229ab

这一切都在小王不知情的情况下发生。 必须明白,浏览器将发送任何有效 Cookie 或身份验证信息,这是设计使然。 您是否注意到,您的电子邮件客户端默认情况下通常不加载图像? 其中一个原因就是为了防范 CSRF。 如果您收到一封 HTML 格式的电子邮件(其中包含类似如下的嵌入式图像标记),则此 URL 将会被请求,并且服务器将会执行该操作(如果您已经通过该网站的身份验证):

<img src='yoursite/createuser.aspx?id=hacked&pwd=hacked' />

如果您碰巧是“yoursite”上已通过身份验证的管理员,浏览器将会顺畅地随任何凭据一起发送该 GET 请求。 服务器将其视为已验证用户的有效请求,并将在您不知情的情况下执行该请求,因为没有有效的图像响应需要呈现在您的电子邮件客户端中。

防范 CSRF 的方法 若要防范 CSRF,您首先需要遵循一些规则:

  1. 确保攻击者不能通过简单地单击 GET 请求链接来重播请求。 针对 GET 请求的 HTTP 规范意味着 GET 请求应该只用于检索,而不应该用于状态修改。
  2. 确保在攻击者已使用 JavaScript 模拟窗体 POST 请求的情况下不能重播请求。
  3. 阻止通过 GET 执行的任何操作。 例如,禁止通过 URL 创建或删除记录。 理想情况下,这些措施需要一些用户交互。 尽管这种方法不能防范更精明的、基于窗体的攻击,但它可以限制大量较容易实现的攻击,例如电子邮件图像示例中描述的攻击类型以及 XSS 遭到破坏的网站中嵌入的基本链接。

通过 Web 窗体防范攻击的处理方式与 ASP.NET MVC 略有不同。 使用 Web 窗体,可以对 ViewState MAC 属性进行签名,这有助于防范伪造,只要您不设置 EnableViewStateMac=false。 您还需要使用当前用户会话对 ViewState 进行签名,并防止将 ViewState 传递到查询字符串中以阻止所谓的一键式攻击(参阅图 8)。

图 8 防范一键式攻击

void Page_Init(object sender, EventArgs e)
{
  if (Session.IsNewSession)
  {
    // Force session to be created;
    // otherwise the session ID changes on every request.
Session["ForceSession"] = DateTime.Now;
  }
  // 'Sign' the viewstate with the current session.
this.ViewStateUserKey = Session.SessionID;
  if (Page.EnableViewState)
  {
    // Make sure ViewState wasn't passed on the querystring.
// This helps prevent one-click attacks.
if (!string.IsNullOrEmpty(Request.Params["__VIEWSTATE"]) &&
      string.IsNullOrEmpty(Request.Form["__VIEWSTATE"]))
    {
      throw new Exception("Viewstate existed, but not on the form.");
    }
  }
}

我在此处分配随机会话值的原因是确保建立会话。 您可以使用任何临时会话标识符,但在您实际创建会话之前,ASP.NET 会话 ID 会随每个请求而发生变化。 此处不能让会话 ID 随每个请求发生变化,因此您必须通过创建新会话将其固定下来。

ASP.NET MVC 包含它自己的一组内置帮助程序,以使用随请求传入的独特令牌来防范 CSRF。 这些帮助程序不仅使用必需的隐藏窗体字段,而且使用一个 Cookie 值,从而增加了伪造请求的难度。 这些保护措施很容易实现,而且绝对有必要融入您的应用程序中。 若要在视图中的 <form> 内添加 @Html.AntiForgery­Token(),请执行以下操作:

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken();
  @Html.EditorForModel();
  <input type="submit" value="Submit" />
}
Decorate any controllers that accept post data with the [Validate­AntiForgeryToken], like so:
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult Index(User user)
{
  ...
}

了解漏洞

本文介绍了跨站点脚本和跨站点请求伪造,这是黑客攻击 Web 应用程序的两种常见方法。 结合上个月介绍的两种攻击方法(SQL 注入和参数篡改),您现在应该对应用程序如何受到攻击有了充分的了解。

您还看到为应用程序构建安全性以防范一些最常见的攻击是多么容易。 如果您已经为软件开发生命周期构建了安全性,那太好了! 如果您还没有这么做,那么现在开始构建是最好不过的了。 您可以基于每个页面/每个模块审核您的现有应用程序,并且大多数情况下,您都可以非常轻松地重构现有应用程序。 使用 SSL 保护您的应用程序以防止有人探查您的凭据。 记得在开发之前、开发过程中以及开发之后考虑安全问题。

Adam Tuliper 是 Cegedim 的软件架构师,具有 20 多年的软件开发经验。 他是国家 INETA 社区发言人,定期在各种会议和 .NET 用户组中发表演讲。 您可以通过 Twitter (twitter.com/AdamTuliper)、博客 (completedevelopment.blogspot.com) 或新的 secure-coding.com 网站了解他的观点。 有关如何防范黑客攻击您的 ASP.NET 应用程序的更多详细信息,请参阅他即将发布的 Pluralsight 视频系列。

衷心感谢以下技术专家审阅了本文:Barry Dorrans