如何建立一个安全的“记住我”功能
有这样一个场景——有个用户初访并登录了你的网站,然而第二天他又来了,却必须再次登录。于是就有了“记住我”这样的功能来方便用户使用,然而有一件不言自明的事情,那就是这种认证状态的”旷日持久“早已超出了用户原本所需要的使用范围。这意味着,他们可以关闭浏览器,然后再关闭电脑,下周或者下个月,乃至更久以后再回来,只要这间隔时间不要太离谱,该网站总会知道谁是谁,并一如既往的为他们提供所有相同的功能和服务——与许久前他们离开的时候别无二致。
我在谈论的就是这个“小家伙”:
看上去是不是很巧很好用?那是当然,但你也将看到,即便使用得当,它能引发的那一茶几悲剧也绝非偶然,少说能有一车皮的人准备告诉你它是如何把事情搞糟的。那就让我们从问题的根源出发吧。
反模式
这事情乍一看似乎很明显,就是说“记住我”功能的应用其实是非常基本的,不是那种很玄乎的东西,可事实上呢?显然不是。
下面我会讲解两个反模式案例和问题的所在,以及谈论如何做是正确的。第一个例子,如图所示,当你登录的时候:
这一切都非常符合标准,但有趣的事情发生在登录之后,让我们来看看cookies:
这个cookie相当的给力吧,尤其是被选中的黄色高亮部分。如果你没勾选“记住我”的话,这些要命的信息是不会被cookie记录的,所以那个功能原本也只是单纯为了方便用户再访的。在图中,我的邮箱地址是赤裸裸的,但密码并非明文。然而不要高兴得太早,对着那串似乎坚不可摧的加密字符定睛一看……咦?等一下,这不是Base64编码吗!关于Base64编码,这是一种可以被完整解码的编码,这意味着你可以随便去哪个编码转换网站比如base64decode.org来做下面这件事:
并不是所有人都把Base64当做“加密”(当然,有的人真会用它来加密重要信息),虽说它确实是一种合理代替ASCII的方法,但事实上这种编码的密码仅仅在刚一进入浏览器后就会立即转变为明文。你可能会质疑道 - 这能有多大问题?无论如何它只是存储在自己的浏览器里,它能怎样?那到底黑客能不能得到它呢?
下面我会介绍两个非常简单的方法,首先提及的内容会和上面说的那种情况有关。Black&Decker暴露了ELMAH日志,在这些日志中存在着完全未经配置的内部服务器错误,而被排除的内容是被记录的内容的几万倍。可是当ELMAH记录了一个异常,在执行处理之前系统也会记录所有的请求头,这意味着这些内容全都进入了cookies。试想那数量庞大的被过滤掉的内容其实全被转存了起来,这简直就等于有了一个用户凭据的数据库。当然他们应该已经修补了这个问题,但它是一个很好的例子,如何轻而易举的利用一个非常简单的配置失误。
接下来是另一个问题:
这个网站是Aussie Farmers Direct(以下统称为AFD网),是一个相当典型的表单登陆。那就登录看看,并且让网站“记住我”,接着就来看cookies吧:
好家伙,还是同样的状况,甚至连Base64编码都没加。要说这一点有多糟糕,来看你可以做这个:
XSS自己的JSON cookie?当然可以这么玩,这样一来,如果你的密码改了但cookie还不变,等你再次登录的时候,它就试图用旧密码登录。嗯?
AFD网并没有暴露他们的ELMAH日志(PHP好样的!)但他们还有其他风险,如XSS。有关上述网站的另一件事是,这些存储在Cookie中的密码并没有被标记成HttpOnly,你可以在右边的cookie列表中看到。这意味着,客户端脚本可以访问这些cookies,这就等于是只要你能让XSS成功,就可以通过让其他用户加载XSS payload,窃取包含密码的cookie,(这样做的有效方法很多)。缺少了HttpOnly属性或许是一时马虎,但问题的核心在于存储在cookie中的密码会很容易地通过其他途径疏漏出去。
还有一个更根本的原因,为什么这些网站会同时在这点上疏忽大意,尽管他们都在保护自己客户在其网站上使用的凭据。每当上述网站的客户勾选了“记住我”功能并向网站上发出请求时,当他们的用户名和密码被网站发送到他们的邮箱时,当他们操作eBay或网银时。要么密码是明文,要么可以通过客户端脚本获取,总之密码总会一丝不挂的躺在浏览器里。大量的人有密码复用(通用)的习惯 ,然而为那些热爱作死的用户承担责任的,却是我们开发者。我不得不说,当我们应对那些用户凭据威胁的时候,需要实施的保护措施要远远超过网站本身。
因此,应该意识到对于如何建立“记住我”功能上的真正误解,下面我们再来看看良性实践。
实例
在安全界混,有句话你应该会很熟悉,“不用你自己试 - 就用那个公认最给力的就行”。这类话在加密和认证计划中非常容易听到,亦可应用在本文课题,借鉴过来深入研究细节。
在一个用Visual Studio 2012建设的ASP.NET MVC 4网站中,你会看到这个:
其他框架有其他的标准模式来实现此功能,但这个更容易作为参考。当我们通过提交上面的表单进行登录时,并且不要求“记住我”,以下的cookie被成功验证,结果返回:
Set-Cookie:.ASPXAUTH=6891A5EAF17A9C35B51C4ED3C473FBA294187C97B758880F9A56E3D335E2F020B86A85E1D0074BDAB2E1C9DBE590AF67895C0F989BA137E292035A3093A702DEC9D0D8089E1D007089F75A77D1B2A79CAA800E8F62D3D807CBB86779DB52F012; path=/; HttpOnly
这是一个简单的身份验证cookie,并捆绑了所有HTTP的数据。这种我专属的cookie被发送后,网站就每次都能知道这是我而且我已经被验证了。我们还能看得更清楚,比如当我们用Chrome’s Cookies collection的时候:
另外,第二个cookie是一个防伪标记,以防止CSRF攻击,并且与我们的认证状态无关。除此之外,有没有其他的cookie。
现在,让我们勾选上“记住我”然后再次登录看看cookie的响应:
Set-Cookie:.ASPXAUTH=3A92DAC7EFF4EE5B2027A13DD8ABEA8254F0A16D8059FCAF60F5533F1B7D99462DDF57320D069A493481978750526DF952D5C9EA0371C84F5CF1BFC0CCA024C2052824D4BA09670A42B85AEC7FFCB4088FC744C6C0A22749F07AF6E65E674A4A; expires=Tue, 02-Jul-2013 00:27:05 GMT; path=/; HttpOnly
来了你看到没?来用Chrome解剖一下看看:
我们现在有了一个有效期为48小时的cookie,如果当它过了期限,它在浏览器关闭时,将被丢弃。让我们来仔细看看。
寻找身份验证Cookie的时限部分
其实,这是一个简单得略显荒谬的安全构造,我都甚至认为它没有写出来的价值,来看一下这里:在这个例子中,“记住我”功能可以简单地归结为,它控制了cookie的时限并且决定某个人能够持续登录多久。
在上面的例子中,ASP.NET默认使用一个会话cookie,或者换句话说,一个cookie,而且没有一个明确的截止日期,因此将在浏览器关闭时强行过期。这是一种方法,另一种是直接置入短保质期,即使浏览器继续使用该cookie,用户也将被自动注销。当然,你也可以在服务器上控制这种行为,你也可以让身份验证cookie的时限不断延长,如果系统正在积极使用由服务器响应增加的时限。
只要保持这个验证cookie有效,特定的人就会被记住。那多久的时效才合适呢?上面的例子中默认为2天,但这对于合法的使用者显然有些过短。而Facebook的cookie却能持续一年。持续时间较短,意味着更少的风险,但更多的不便,持续时间较长,使得它更容易为用户增加潜在的风险。让我们更进一步的看看这个风险。
长期认证状态的利用
当在被认证之前,你的会话无法被劫持。废话,这是当然的!但认真看的话,像在上面AFD网的那种情况下,该cookie将在6个月后到期,与此同时它没有HTTP only的标记,这样一来他们网站的XSS漏洞可以为攻击者提供半年的时间去获取并使用用户的凭证。同样的情况,如果时限为1个月,他们仍会有一些严重的漏洞,但上述攻击的机会实实在在地得到了削减。
另一方面,Black&Decker网有一个相对短的——一周的期限。在他们暴露着ELMAH日志的情况下,依然有一系列的重大疏漏,但除非有人在一周前已经用“记住我”登录了网站,并且触发了那个默认配置的漏洞,否则凭据不会被泄露。如果你想找个网站自己试试看的话,就算是一个你已经登录过的网站,也可以看到存在时限风险的cookie。
所以说一切有关身份验证的cookie如果想要保护好用户凭据的话,HttpOnly的安全属性是和严谨的安全态度必须的。虽然所有经典的劫持威胁仍然存在,不过,解决这些cookie上的问题也是绝不容忽视的。
归根结底,这是一个权衡,需要考虑的因素如攻击者要取得的数据的价值,在加强验证安全性时对于用户使用的便捷性和网站安全配置所造成的负面影响。例如,Facebook中存在着一些非常有用的社会性的用户数据,而用户又非常希望无延时般的响应速度,在此之上他们还对自己的账户安全上进行了大笔的投资。而对于AFD网,在持有用户个人身份数据以及财务信息的同时,提供了用户所要求的安全验证服务,能看出用户本身也是有相关安全意识的。他们有着迥然不同的风险,这两个网站对于身份验证cookie的时限策略应该是完全不同的。
强化
或许有些同学会觉得AUTH cookie很无解,有关安全性的东西总会有一种更好的解决方案,但这是有代价的,安全性取决于你愿意付出的的时间、金钱、便利性,而且也总会有人告诉你,你做错了!让我们来看看关于使用AUTH cookie时限的一些可能的加强方法。
对于一个长期有效的AUTH cookie,问题在于他们需要有效地保持用户身份的验证和面对如CSRF或clickjacking等攻击风险。当然,还有很多需要利用长期cookie的风险并没有列出,但这并不影响围绕防范方法的讨论。还有一点就是当一个专用cookie为用户在服务器上提供过有效认证之后,在返回时它还可以重新开启另一个认证会话。虽然最初的会话会迅速到期,但关键是重启的新会话,它会因为用户勾选了“记住我”并再次登录而进行另外的验证。
一种验证方式包括利用用户的IP地址/用户代理/其他显著特点来限制“记住我”的cookie。这能提供一些对cookie劫持的防御。当然,要在合法使用的情况下进行这些变更。特别是在移动网络中这类的情况并不少见——用不同的IP回访一个网站。你的ISP不一定总会提供静态的IP地址。至于用户代理,还有浏览器差异,如Chrome和Firefox的更新简直恍如隔日。然而除非你刻意去挑选某些优质的代理,否则使用代理将是一个危险的做法。
还有一种程序化的手段,就是保持“记住我”cookie和身份验证cookie的分离,并使用前者重新验证用户的身份,但要额外施加一些限制。实际情况是,某个身份的自动认证状态的过程,一定会遵循安全模型。缓和措施的结果就是,它会在自动重新验证之前,向用户再次索要凭据。这并非创新之举,你或许会在进行例如网银汇款时遇到过此类功能。在这里,我们说身份验证的用户面临的风险很大,因为这种方式很容易被劫持和欺骗。
至于其他加强方法,我们可以在“记住我”cookie被使用后进行复位。这也等于让它在服务器端无效,而需要一个独特且持久的cookie值,例如存在于数据库和cookie间的一个随机数。这样有利于确保cookie不会被攻击者用下面的方式获得。在这里的文章中,作者谈论了一些关于这类模式的缓解方法。但你需要付出一些额外的工作,并在某种程度上给正常的用户带来不便(例如用户无法跨越多个电脑使用并“记住”自己的凭据)。
最后一件值得提一句的是,同一帐户下的管理原则,它需要我们去关注并且和“记住我”功能有关。例如,允许单一用户的多个会话同时验证?或者用户一旦更改了密码要不要将会话断开?管理员可不可以结束验证会话?出现了各种各样的问题,不过这里要讨论的仅仅是关于如何复原。
什么时候不该用“记住我”功能?(以及一些替代功能)
有时候,允许一个经过认证的用户保持认证状态很长一段时间是毫无意义的。例如银行,在常规的使用情况下,当你想图省事非要进行自动登录时,在你走人以后留在那里的是一个未登出的浏览器,风险多大不用我说了吧。
但其实还是有一些中间地带可以采取下面这种做法:
这个登陆框并不是看上去的那么弱,当你用“记住我”登录过后,再次回访的时候,网站的会话就过期了,你会看到:
用户名这个样子可不是我起的,是因为它连同其他一些数据在cookie中存了三个月。坦白地说,这么做没有意义,因为记住用户名不难啊!
但是,这种情况既不能全否也不能全部肯定,这是个灰色的中间地带。例如,假设在通过“记住我”功能恢复会话时,重新认证是在主进程之前启动的。这或许是一个两全齐美的做法。
总结
这个也不例外,总会有些功能看上去似乎是一个好主意,而且它通常是很容易做好的,至少可以适合大多数的项目。坦率来讲,前面两个例子仍然莫名其妙的让人头疼,特别是当你考虑到它本来只是用于延长cookie时限的。
另外,这篇文章的重点并不局限于分解“记住我”功能的安全结构,你可能能够自行解决自己的cookie凭据问题,但结合起ELMAH和缺少HTTP only属性以及XSS漏洞的情况考虑,时刻警惕一个不经意的行为,(行为)虽然看似无害但却很可能造成一个严重的安全风险。
原文 How to build (and how not to build) a secure “remember me” feature
翻译 IDF实验室 张俊
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。