第一部分:如何登录
我们假设您已经知道如何构建一个登录+密码的HTML表单,将值POST到服务器端的脚本进行身份验证。下面的部分将涉及实用的身份验证模式以及如何避免最常见的安全陷阱。
使用HTTPS还是不使用HTTPS?
除非连接已经安全(即通过SSL/TLS隧道传输的HTTPS),否则您的登录表单值将以明文形式发送,这允许任何窃听浏览器和Web服务器之间的线路的人在传递时读取登录信息。政府通常会进行此类窃听,但总的来说,我们不会处理其他所有权线路,只需说一句:请使用HTTPS。
实质上,保护登录过程免受窃听/数据包嗅探的唯一实用方法是使用HTTPS或其他基于证书的加密方案(例如TLS)或经过验证的挑战-响应方案(例如基于Diffie-Hellman的SRP)。任何其他方法都可以轻松地被窃听攻击者绕过。
当然,如果您愿意有点不切实际,您也可以采用某种形式的双因素认证方案(例如Google Authenticator应用程序、物理的“冷战风格”代码簿或RSA密钥生成器dongle)。如果正确应用,即使使用不安全的连接,这也可以起作用,但很难想象开发人员愿意实施双因素身份验证而不是SSL。
(不要)自己编写JavaScript加密/哈希
考虑到在网站上设置SSL证书的感知成本(尽管现在
可避免)和技术难度,一些开发人员会尝试自己编写浏览器中的哈希或加密方案,以避免通过不安全的信道传递明文登录信息。
虽然这是一个高尚的想法,但除非它与上述方案之一相结合,否则它基本上是无用的(并且可能存在
安全漏洞),即使用强加密保护线路或使用经过验证的挑战-响应机制(如果您不知道这是什么,请知道这是数字安全中最难以证明、设计和实施的概念之一)。
尽管对于密码泄露,散列密码可以有效,但它容易受到重放攻击、中间人攻击/劫持(如果攻击者可以在未经安全保护的 HTML 页面到达浏览器之前注入几个字节,他们可以简单地在 JavaScript 中注释掉散列),或暴力攻击(因为您将用户名称、盐和散列密码全部交给攻击者)。
反人类的 CAPTCHA
CAPTCHA 旨在防止一种特定类型的攻击:自动字典/暴力试错攻击,没有人工操作员。毫无疑问,这是一个真正的威胁,然而,有一些处理方式可以无缝地解决这个问题,而不需要使用 CAPTCHA,具体来说是正确设计的服务器端登录限制方案-我们稍后会讨论这些。
要知道,CAPTCHA 实现并不相同;它们通常不能被人类解决,大多数实际上对机器人无效,所有实现都无法对廉价的第三世界劳动力产生影响(根据
OWASP,当前的血汗工厂率为每 500 次测试 12 美元),而且某些实现在某些国家可能是技术上非法的(请参见
OWASP 认证防伪指南)。如果必须使用 CAPTCHA,请使用 Google 的
reCAPTCHA,因为它根据定义是 OCR-hard(因为它使用已经 OCR-misclassified 的书籍扫描),并且非常努力地使用户友好。
个人而言,我倾向于认为验证码很烦人,只有在用户多次登录失败且限制延迟已达到最大值时才使用它们作为最后的手段。这种情况很少发生,因此可以接受,并且它会加强整个系统。
存储密码/验证登录
经过近年来广泛报道的黑客攻击和用户数据泄漏事件,这可能已经是常识了,但必须说一下:不要在数据库中以明文形式存储密码。用户数据库经常被黑客攻击、泄漏或通过SQL注入获取,如果您以原始的明文密码进行存储,那么登录安全性立即结束。
因此,如果无法存储密码,如何检查从登录表单POST的登录+密码组合是否正确?答案是使用
密钥派生函数进行哈希。每当创建新用户或更改密码时,将密码通过KDF(例如Argon2、bcrypt、scrypt或PBKDF2)运行,将明文密码(“correcthorsebatterystaple”)转换为一个长的、看起来随机的字符串,这样在数据库中存储更安全。要验证登录,您需要对输入的密码再次运行相同的哈希函数,这次传入盐并将生成的哈希字符串与存储在数据库中的值进行比较。Argon2、bcrypt和scrypt已经将盐与哈希一起存储。请查看sec.stackexchange上的
文章以获取更详细的信息。
一个盐的原因是单纯的哈希不足以保护哈希值免受
彩虹表攻击。盐有效地防止两个完全匹配的密码被存储为相同的哈希值,从而防止在攻击者执行密码猜测攻击时整个数据库一次性被扫描。
密码存储不应使用加密哈希,因为用户选择的密码不够强大(即通常不包含足够的熵),攻击者可以在相对较短的时间内完成密码猜测攻击。这就是为什么要使用KDF的原因 - 这些方法有效地"延长密钥", 这意味着每个攻击者猜测密码都会导致多次重复哈希算法,例如10,000次,从而使攻击者猜测密码变慢10,000倍。
会话数据 - "您已登录为Spiderman69"
一旦服务器已经验证了登录和密码与用户数据库匹配,系统需要一种方式来记住浏览器已经通过身份验证。这个事实只能在会话数据中存储在服务器端。
如果你不熟悉会话数据,这是它的工作原理:一个单一的随机生成的字符串被存储在一个过期的cookie中,并用于引用一个数据集合 - 会话数据 - 这些数据被存储在服务器上。如果你使用的是MVC框架,这无疑已经处理好了。
如果可能的话,在将会发送到浏览器的会话cookie中设置安全和HTTP Only标志。HttpOnly标志可提供一定保护,防止通过XSS攻击读取cookie。安全标志确保仅通过HTTPS发送cookie,从而防止网络嗅探攻击。cookie的值不应是可预测的。如果出现引用不存在的会话的cookie,则应立即替换其值以防止
会话固定。
客户端还可以维护会话状态。这可以通过使用JWT(JSON Web Token)等技术实现。
第二部分:如何保持登录状态-臭名昭著的“记住我”复选框
长时间登录cookie(“记住我”功能)是一个危险区域;一方面,当用户知道如何处理它们时,它们完全与常规登录一样安全;另一方面,当不小心使用它们的用户在公共计算机上使用它们并忘记注销时,它们是巨大的安全风险,并且他们可能不知道浏览器cookie是什么或如何删除它们。
个人而言,我喜欢为我经常访问的网站使用长时间登录,但我知道如何安全地处理它们。如果您确定您的用户也知道相同的内容,则可以放心使用长时间登录。如果没有 - 那么您可能认同这样的理念:如果用户对其登录凭据不小心,他们就会自食其果。我们也不会去用户家里撕掉那些密码写在显示器边缘上的让人面红耳赤的便笺。
当然,有些系统无法承受任何账户被黑客攻击的风险;对于这样的系统,你无法为持久登录提供合理的解释。
如果您决定实现持久登录 cookie,则需要按照以下步骤操作:
1.首先,花些时间阅读
Paragon Initiative's article。您需要正确处理很多元素,该文章很好地解释了每个元素。
2.并且要再次强调最常见的一个问题,
不要将持久登录 cookie(令牌)存储在数据库中,只需存储其哈希值!登录令牌等同于密码,因此如果攻击者得到了您的数据库,他们可以使用令牌登录任何帐户,就像明文登录密码组合一样。因此,在存储持久性登录令牌时,请使用哈希(根据
https://security.stackexchange.com/a/63438/5002,弱哈希就足够了)。
第三部分:使用密保问题
不要实现“密保问题”。 “密保问题”功能是安全反模式。阅读MUST-READ列表中的链接4的论文。你可以问问萨拉·佩林(Sarah Palin),她的雅虎邮件帐户在之前的总统竞选中被黑客攻击,因为她的安全问题的答案是...“Wasilla High School”!
即使用户指定问题,大多数用户选择以下两个选项的可能性非常高:
一个“标准”的秘密问题,如母亲的娘家姓或最喜欢的宠物
一个简单的琐事,任何人都可以从他们的博客、LinkedIn资料或类似的地方获得
任何比猜测他们的密码更容易回答的问题。对于任何合理的密码来说,你能想到的每一个问题都是这样
总之,在几乎所有形式和变化中,安全问题本质上都是不安全的,不应出于任何原因在身份验证方案中使用。
安全问题为什么会存在于公共领域?真正的原因是它们方便地节省了一些支持电话的费用,以及无法访问其电子邮件以获取重新激活代码的用户。这是以安全和萨拉·佩林的声誉为代价的。值得吗?可能不值得。
第四部分:忘记密码功能
我已经提到过为什么您永远不应该使用安全问题来处理忘记/丢失的用户密码;同时也不用说您永远不应该通过电子邮件发送用户的实际密码。在这个领域,还有至少两个常见的陷阱需要避免:
- 不要将忘记的密码重置为自动生成的强密码 - 这些密码非常难记,这意味着用户必须更改密码或将其写下来 - 例如在显示器边缘的明黄色便笺上。相反,只需让用户立即选择新密码 - 这正是他们想做的。 (唯一的例外可能是如果用户普遍使用密码管理器来存储/管理密码,否则很难记住这些密码)。
- 始终对数据库中的丢失密码代码/令牌进行哈希处理。 再次强调,此代码是另一个密码等效项的示例,因此必须进行哈希处理,以防攻击者接触到您的数据库。当请求丢失密码代码时,请将明文代码发送到用户的电子邮件地址,然后对其进行哈希处理,在数据库中保存哈希值 - 并丢弃原始代码。就像密码或持久登录令牌一样。
最后注意:始终确保输入“丢失密码代码”的界面至少与登录表单本身一样安全,否则攻击者将简单地使用该界面获得访问权限。确保生成非常长的“丢失密码代码”(例如16个区分大小写的字母数字字符)是一个好的开始,但考虑为登录表单本身添加相同的限制方案。
第五部分:检查密码强度
首先,您需要阅读这篇小文章进行现实检查:500个最常见的密码
好的,所以这个列表可能不是任何系统中最常见密码的
规范列表,但它很好地说明了当没有强制策略时人们选择密码的糟糕程度。此外,与公开可用的最近被盗密码分析相比,该列表看起来非常接近家庭。
因此:如果没有最低密码强度要求,则有2%的用户使用前20个最常见的密码之一。意思是:如果攻击者只尝试20次,您网站上50个帐户中将有1个易受攻击。
为了防止这种情况,需要计算密码的熵,然后应用阈值。国家标准与技术研究院(NIST)
Special Publication 800-63提出了一套非常好的建议。结合字典和键盘布局分析(例如,“qwertyuiop”是一个糟糕的密码),可以在18位熵水平上
拒绝99%的所有选择不当的密码。仅仅计算密码强度并向用户
显示可视化强度计是好的,但不足够。除非得到强制执行,否则许多用户很可能会忽略它。
我建议阅读Randall Munroe的Password Strength xkcd,以了解高熵密码的用户友好性。
使用Troy Hunt的Have I Been Pwned API检查用户密码是否已被泄露。
第六部分:更多内容 - 或者:防止快速登录尝试
首先,看一下这些数字:Password Recovery Speeds - How long will your password stand up
如果你没有时间查看该链接中的表格,请看下面的列表:
即使使用算盘,破解一个弱密码也需要几乎没有时间
如果密码不区分大小写,那么破解一个由字母和数字组成的9位密码几乎没有时间
如果密码长度少于8个字符,即使是由符号、字母和数字等复杂的大小写混合密码,也只需要几乎没有时间(桌面PC可以在数天甚至数小时内搜索整个7个字符的密钥空间)
但是,如果你每秒只能尝试一次,即使是6位密码,破解它需要很长时间!
那么我们从这些数字中可以学到什么呢?很多,但我们可以关注最重要的部分:防止大量快速连续的登录尝试(即暴力破解攻击)实际上并不那么困难。但是,要正确地防止它并不像看起来那么容易。
一般来说,你有三种选择,都可以有效地防止暴力攻击(以及字典攻击,但由于你已经采用了强密码策略,它们不应该是问题):
在N次失败尝试后呈现CAPTCHA(非常烦人且通常无效 - 但我在这里重复了)
锁定帐户并在N次失败尝试后要求电子邮件验证(这是一个等待发生DoS攻击)
最后,登录节流:即,在N次失败尝试后设置时间延迟(是的,DoS攻击仍然可能发生,但至少它们不太可能,并且更加复杂)。
最佳实践#1:短暂延迟随着失败尝试次数而增加,例如:
- 1次失败尝试=无延迟
- 2次失败尝试= 2秒延迟
- 3次失败尝试= 4秒延迟
- 4次失败尝试= 8秒延迟
- 5次失败尝试= 16秒延迟
- 等。
对此进行DoS攻击将非常不切实际,因为结果锁定时间略大于先前锁定时间之和。
澄清一下:延迟不是在将响应返回给浏览器之前的延迟。它更像是一个超时或冷却期,在此期间,特定帐户或特定IP地址的登录尝试将根本不被接受或评估。也就是说,正确的凭据不会返回成功的登录,而不正确的凭据也不会触发延迟增加。
最佳实践 #2: 在N次失败尝试后执行中等长度的时间延迟,例如:
- 1-4次失败尝试=无延迟
- 5次失败尝试=15-30分钟延迟
对这种方案进行DoS攻击将非常不切实际,但确实可行。此外,需要注意的是,这样长时间的延迟可能会让合法用户感到非常恼火。健忘的用户会讨厌你。
最佳实践 #3: 结合两种方法 - 一种是在N次失败尝试后执行固定的短时间延迟,例如:
- 1-4次失败尝试=无延迟
- 5次或更多失败尝试=20秒延迟
或者,采用逐步增加的延迟和固定的上限,例如:
- 1次失败尝试=5秒延迟
- 2次失败尝试=15秒延迟
- 3次或更多失败尝试=45秒延迟
这个最终方案来自于OWASP最佳实践建议(MUST-READ清单中的链接1),即使它确实有一定的限制性,也应该被视为最佳实践。
然而,作为一个经验法则,我会说:你的密码策略越强,你就越不需要用延迟来干扰用户。如果你要求强密码(区分大小写的字母数字+必须包含数字和符号)长度为9个或更多字符,你可以在激活节流之前给用户2-4次无延迟的密码尝试。
这种最终的登录限制方案很难遭到DoS攻击。为了保证合法用户不会受到影响,始终允许持久性(cookie)登录(和/或CAPTCHA验证的登录表单)通过,即使攻击正在进行中。这样,非常不切实际的DoS攻击变成了极其不切实际的攻击。
此外,对于管理员帐户,执行更积极的限制是有意义的,因为这些是最有吸引力的入口点。
除此之外,更高级的攻击者将尝试通过“分散他们的活动”来规避登录限制:
- 将尝试分布在僵尸网络上以防止IP地址标记
- 与其选择一个用户并尝试50,000个最常见的密码(由于我们的限制,他们无法这样做),他们将选择最常见的密码并针对50,000个用户进行尝试。这样,他们不仅可以规避像CAPTCHA和登录限制这样的最大尝试措施,而且他们的成功率也会提高,因为第一位最常见的密码比第49,995位更有可能。
- 为每个用户帐户间隔30秒的登录请求,以逃过雷达
在这里,最佳实践是
记录全系统失败的登录次数,并使用站点坏登录频率的运行平均值作为您对所有用户强制执行的上限的基础。
太抽象了?让我来重新表述一下:
假设您的网站在过去3个月中平均每天有120次错误登录。使用该值(运行平均值),您的系统可能将全局限制设置为3倍,即24小时内失败尝试次数达到360次。然后,如果所有帐户的失败尝试总数在一天内超过该数字(或者更好的是,监视加速率并触发计算阈值),它将激活系统范围的登录限制 - 这意味着所有用户都会有短暂延迟(仍然除了cookie登录和/或备用CAPTCHA登录)。
我还发布了一个问题,其中更多细节和如何避免棘手陷阱的真正好的讨论,以抵御分布式暴力攻击
第八部分:双因素身份验证和认证提供商
凭据可能会被攻击者获取,无论是通过漏洞、密码被写下并丢失、带有密钥的笔记本电脑被盗或用户输入登录信息到网络钓鱼网站中。登录可以通过双因素身份验证进一步保护,该身份验证使用带外因素,如从电话呼叫、短信、应用程序或加密狗接收的一次性代码。几家提供双因素身份验证服务的公司。
认证可以完全委托给单点登录服务,其中另一个提供商处理收集凭据的问题。这将问题推向了可信赖的第三方。Google和Twitter都提供基于标准的SSO服务,而Facebook提供了类似的专有解决方案。
关于Web身份验证的必读链接
- OWASP身份验证指南 / OWASP身份验证备忘单
- Web客户端身份验证的Dos and Don'ts(非常易读的MIT研究论文)
- 维基百科:HTTP cookie
- 回退身份验证的个人知识问题:Facebook时代的安全问题(非常易读的伯克利研究论文)
HttpOnly
是一个非常有用的 cookie 标记,它可以防止基于 JavaScript 的 cookie 盗取,这是 XSS 攻击的一个子集。建议在某处提到此标记。 - Alan H.