在日常的互联网生活当中,我们几乎都离不开用户验证登陆功能,例如:登陆微博,Gmail,博客园,Stackoverflow等网站,这给我们带来了一些不便,就是要管理一堆的用户名和密码,也许有人会说现在很多网站都提供授权验证登陆功能,其中使用最广泛的是OAuth验证机制;在某些情况下,例如一些论坛网站提供微博账户登陆功能,它的实现的却方便了用户,因为它为用户开放服务和重用现有的账户,把认证过程转给外部服务。
但是作为开发者的我们还必须考虑到一些用户会采用内部应用程序授权,所以我们还需要提供内部用户验证登陆功能。
在应用程序中,如果验证和会话(Session)管理的功能没有正确实现时,导致攻击者可以窃取用户密码,会话令牌或利用其他漏洞来伪装成其他用户身份。
在ASP.NET中,会话状态(Session state):是通过在内存中以字典或哈希表的数据结构来存储用户的会话;例如:以哈希表(Key/Value)形式来存储,那么根据Key值(SessionId)检索指定的Value值(Session)的时间复杂度是O(1)。
我们知道HTTP是无状态协议,简而言之,由于Web服务器不会保留以前请求过程中所使用的变量值和任何信息,所以它会把每个页面请求都认为是独立的;但我们知道当用户成功登陆之后,再访问其他页面时是无需重新登陆就可以访问授权页面了,这是由于在Web服务器和浏览器之间维持着一个Session state。
ASP.NET 会话状态(Session state)将来自限定时间范围内的同一浏览器的请求标识为一个会话,并提供用于在该会话持续期间内保留变量值的方法。默认情况下,将为所有 ASP.NET 应用程序启用 ASP.NET 会话状态。
请求持久化的实现:当会话开始时,ASP.NET应用程序通过向客户端提供一个唯一的密钥(SessionId)来管理会话状态(Session state),这个密钥(SessionId)存储在客户端向服务器发送的每个HTTP请求的cookie中;然后,服务器可以从cookie中读取密钥(SessionId),并重新存储到服务器的会话状态。
现在我们使用Fiddler查看HTTP请求中的数据如下:
图1 HTTP请求
我们看到在HTTP请求中,Header的Cookie里包含了SessionId值,然后Web服务器就可以获取到Cookie中的SessionId值,由于每个会话都拥有一个唯一的SessionId值,所以Web服务根据SessionId来识别不同的会话。
接下来让我们通过具体的例子说明白吧!
现在让我们通过一个简单的登录页面来说明会话(Session)的原理:
首先我们设计一个简单的登界面。
图2登陆界面
当登陆成功之后,显示信息和跳转页面。(注意:登陆和提示在同一个页面)
图3登陆成功
/// <summary> /// Handles the Click event of the btnLogin control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="System.EventArgs"/> /// instance containing the event data.</param> protected void btnLogin_Click(object sender, EventArgs e) { lblUsername.Visible = false; lblpassword.Visible = false; txtPassword.Visible = false; txtUsername.Visible = false; btnLogin.Visible = false; //// After successful authentication, save username into session. Session["Username"] = txtUsername.Text; //// Shows message and redirect link. lblMsg.Visible = true; hlpage.Visible = true; }
图4跳转页面
上面的示例代码中,我们并没有给出具体的验证实现,由于我们关注的是用户验证成功之后,程序把用户信息存储到Session中,当页面发生跳转时,通过Session获取用户信息。
现在我们在cookies页面中,查看当前SessionId的值如下:
图5会话id
由于数据通过会话状组织起来,会话是由客户端浏览器指定,并且通过浏览器的cookie持久化存储,现在我们把以上Url复制到其他浏览器的地址栏中。
图6会话
如上图所示,在Chrome浏览器中不能获取我们之前保存在Session中的Username,而且我们查看Chrome中的SessionId值如下:
图7会话id
现在我们知道Session可以通过Cookie持久化存储,但由于不同的浏览器都会保存各自的Cookie,导致Session无法跨浏览器。
现在我们不把Session保存在Cookie中,而是通过Url直接传递到服务器中,这就可以实现Session跨浏览器,接下来让我们看一下ASP.NET中的实现吧!
<system.web> <!--Don't use cookies to persist session--> <sessionState cookieless="true" /> </system.web>
我们在WebConfig中设置cookieless="true",当再次运行Logon页面时,我们发现Url中嵌入了一个字符串,这是由于cookieless="true"不把Session保存在Cookie中,而是通过在Url中嵌入SessionId进行传递。
图8 Url传递SessionId
当我们输入用户名和密码登陆后,显示用户登陆成功如下所示:
图9 Url传递SessionId
接着我们点击链接页面跳转到Redirect,我们发现SessionId和刚开始登陆时候使用的是同一个,这样Web服务器就可以根据SessionId获取会话信息。
图10 Url传递SessionId
由于我们在webconfig中设置cookieless="true",这样就实现了跨浏览器访问授权页面,但这也存在一个严重的安全问题――Session劫持。
假如我们都在页面的Url中嵌入SessionId,一旦SessionId被攻击者获取他们就无需再登录就可以访问授权页面了,这对用户来说可是一场灾难。
也许有人想如果在Url中嵌入SessionId冒着很大的风险,还不如直接把SessionId存储在Cookie中;但是使用Cookie持久化也可能发生Session劫持,由于Cookie是一个Key/Value的集合,攻击者可以通过类似XSS或其他攻击获取用户的Cookie。
幸运的是ASP.NET中把所有的Cookie都标记为HttpOnly,这使得攻击者无法通过客户端脚本(如document.cookie)获取浏览器的Cookie,例如:通过跨站脚本(XSS, Cross Site Script)漏洞进行攻击。
HttpOnly是Set-Cookies中一个附加标记,如果在HTTP响应头中包含HttpOnly标志(可选),那么将不能通过客户端脚本访问Cookie(再次浏览器是否支持此标志)。因此,即使存在跨站点脚本(XSS)漏洞,也不能访问浏览器的Cookie。(HttpOnly是由微软在2000年IE6 Sp1中率先发明并予以支持的)
从表中我们可以看出,目前主流的浏览器,除了Android之外,几乎都无一例外对HttpOnly属性予以了支持。
Browser |
Version |
Prevents Reads |
Prevents Writes |
Microsoft Internet Explorer |
10 |
Yes |
Yes |
Microsoft Internet Explorer |
9 |
Yes |
Yes |
Microsoft Internet Explorer |
8 |
Yes |
Yes |
Microsoft Internet Explorer |
7 |
Yes |
Yes |
Microsoft Internet Explorer |
6 (SP1) |
Yes |
No |
Microsoft Internet Explorer |
6 (fully patched) |
Yes |
Unknown |
Mozilla Firefox |
3.0.0.6+ |
Yes |
Yes |
Netscape Navigator |
9.0b3 |
Yes |
Yes |
Opera |
9.23 |
No |
No |
Opera |
9.5 |
Yes |
No |
Opera |
11 |
Yes |
Unknown |
Safari |
3 |
No |
No |
Safari |
5 |
Yes |
Yes |
iPhone (Safari) |
iOS 4 |
Yes |
Yes |
Google's Chrome |
Beta (initial public release) |
Yes |
No |
Google's Chrome |
12 |
Yes |
Yes |
Android |
Android 2.3 |
Unknown |
Unknown |
现在,我们对验证破坏和会话管理有了初步的了解,接下来我们使用ASP.NET中提供的membership和role providers来解决用户验证问题吧!
.NET Framkework 2.0中微软提出了提供者模式(Provider)而且membership和role provider也是基于该模式实现的,在NET2.0或之后的版本,已经提供解决涉及到用户身份验证和访问权限管理方法,它们都是提供者模式的。 而且常用功能如帐户的创建、验证、授权和密码提醒等,这方便了开发者无需从头开始创建,减少引入不安全的代码。
相信许多人都实现过页面登陆功能,这个看似简单的功能,但实际实现起来时很复杂的,由于.NET中提供LoginView控件已经实现了功能,它整合了用户身份验证和访问管理。
除了LoginView控件之外,我们还可以在Visual Studio工具箱中找到其他类似的控件。这些控件都集成了一些常用的功能(如:验证登陆,登陆状态和修改密码等),这样我们就无需编写重复的代码就可以很好的实现用户验证登陆功能。
图11 Login控件
由于会话(Session)是有生命周期的,在发生会话劫持时,如果该会话已经过期,那么就可以降低被劫持的风险,我们是否应该把会话的生命周期设置的很短呢?回答是否定的,如果会话很快就过期,那么用户就要不断重新登录才能访问授权页面,这样的用户体验效果是很差;所以我们必须在衡量风险和体验效果前提下设置timeout值。
默认情况下,ASP.NET中在不活动情况下Session的timeout值为30分钟。我们可以通过在web.config中设置Session的timeout属性来修改过期时间,这里我们把Session过期时间设置为10分钟,具体实现如下:
<system.web> <!--Setting session expiration date--> <sessionState timeout="10" /> </system.web>
前面我们在程序中设置了会话过期时间,当然用户也可以通过手动Log out使会话过期。
应该牢记在心充足的数据加密可以确保用户身份验证数据安全性,实现安全认证凭据披露的影响明显和加密的缓解需要发生在两个关键层的认证过程:
1.通过存储持久数据层的加密(关于数据加密请参看这里和这里)。
2.正确使用 SSL。
Cookie一个不太常被使用的属性是Secure. 这个属性启用时,浏览器仅仅会在HTTPS请求中向服务端发送Cookie内容。如果你的应用中有一处非常敏感的业务,比如登录或者付款,需要使用HTTPS来保证内容的传输安全;而在用户成功获得授权之后,获得的客户端身份Cookie如果没有设置为Secure,那么很有可能会被非HTTPS页面中拿到,从而造成重要的身份泄露。所以,在我们的Web站点中,如果使用了SSL,那么我们需要仔细检查在SSL的请求中返回的Cookie值,是否指定了Secure属性。
本文介绍了验证破坏和会话劫持攻击,通过具体的例子介绍了会话(Session)在浏览器中的工作原理,通过Cookie来保持身份认证的服务端状态,这种保持可能是基于会话(Session)的,也可以是通过在Url中嵌入SessinId持久化。
接着我们介绍了.NET中用户验证功能——ASP.NET membership和role providers的用户验证功能。
最后,介绍通过设置合适的会话有效期和加密验证信息可以更好的确保用户验证的安全性。
http://msdn.microsoft.com/en-us/library/ms178581(v=vs.90).aspx
http://msdn.microsoft.com/en-us/library/a28ctsa5.aspx
http://www.infoq.com/cn/articles/cookie-security