执行安全的验证解决方案需要同时满足几个关键安全目标,许多时候也需要牺牲其他目标,如功能、易用性和总成本。有些时候,“更加”安全实际上可能适得其反。例如,强迫用户设置超长密码并频繁修改密码往往促使他们将密码记录下来(因而导致密码泄露)。
鉴于验证漏洞的多样性,以及应用程序需要采取非常复杂的防御措施以减轻所有这些漏洞的危害,许多应用程序设计者与开发者选择接受某些威胁,以集中精力阻止最严重的攻击。在实现这种防御平衡的过程中,我们需要考虑以下因素。
应用程序所提供功能的安全程度。
用户对不同类型的验证控制的容忍和接受程度。
支持一个不够友好的用户界面系统所需的成本。
竞争性解决方案相对于应用程序可能产生的收入方面的金融成本或它所保护资产的价值。
我们将在本节说明阻止各种针对验证机制攻击的最有效方法,然后让读者自行决定哪种防御措施最适合他们的特殊需求。
应强制执行适当的最小密码强度要求。这些要求包括:最小密码长度,使用字母、数字和排版字符,同时使用大、小写字符,避免使用字典中的单词、名称和其他常见密码,避免以用户名为密码,避免使用和以前的密码相似或完全相同的密码。和大多数安全措施一样,不同的密码强度要求适用于不同类型的用户。
应使用唯一的用户名。
系统生成的任何用户名和密码应具有足够的随机性,其中不包含任何顺序,即使攻击者访问大量连续生成的实例也无法对其进行预测。
允许用户设置足够强大的密码。例如,应允许其设置长密码,允许在密码中使用各种类型的字符。
应以不会造成非授权泄露的方式创建、保存和传送所有证书。
应使用公认的加密技术(如SSL)保护客户端与服务器间的所有通信。既无必要也不需要使用定制解决方案保护传输中的数据。
如果认为最好在应用程序的不需验证的区域使用HTTP,必须保证使用HTTPS加载登录表单,而不是在提交登录信息时才转换到HTTPS。
只能使用POST请求向服务器传输证书。绝不能将证书放在URL参数或cookie中(即使临时放置也不行)。绝不能将证书返还给客户端,即使是通过重定向参数传送也不行。
所有服务器—客户端应用程序组件应这样保存证书:即使攻击者能够访问应用程序数据库中存储的所有相关数据,他们也无法轻易恢复证书的原始值。达到这种目的最常用的方法是使用强大的散列函数(如至本书截稿时的SHA-256函数),并对其进行“加salt处理”以降低预先计算的离线攻击(precomputed offline attack)的危害。该salt应特定于拥有密码的账户,以防止攻击者重播或替换散列值。
一般来说,客户端“记住我”功能应仅记忆如用户名之类的非保密数据。在安全要求较低的应用程序中,可适当允许用户选择一种工具来记住密码。在这种情况下,客户端不应保存明文证书(应使用密钥以可逆加密的形式保存密码,且只有服务器知道这个密钥);并向用户警告直接访问他们的计算机或远程攻破他们计算机的攻击者可能造成的风险。应特别注意消除应用程序中存在的可用于盗窃其中保存的证书的跨站点脚本漏洞(请参阅第12章了解相关内容)。
应使用一种密码修改工具(请参阅6.4.6节),要求用户定期修改其密码。
如果以非正常交互的形式向新建账户分配证书,应以尽可能安全的形式传送证书,并设置
时间限制,要求用户在第一次登录时更改证书,并告诉用户在初次使用后销毁通信渠道。
应考虑在适当的地方使用下拉菜单而非文本字段截取用户的一些登录信息(如值得纪念的词中的一个字母)。这样做可防止安装在用户计算机上的键盘记录器截获他们提交的所有数据。(但是,还请注意,简单的键盘记录器只是攻击者用于截获用户输入的一种手段。如果攻击者已经攻破用户的计算机,那么从理论上讲,他就能够记录计算机上发生的各种类型的事件,包括鼠标活动、通过HTTPS提交的表单以及截屏。)
应确认完整的密码。也就是说,区分大小写,不过滤或修改任何字符,也不截短密码。
应用程序应在登录处理过程中主动防御无法预料的事件。例如,根据所使用的开发语言,应用程序应对所有API调用使用“全捕获”型异常处理程序(catch-all exception handler)。这些程序应明确删除用于控制登录状态的所有会话和方法内部数据(method-local data),并使当前会话完全失效。因此,即使攻击者以某种方式避开验证,也会被服务器强制退出。
应对验证逻辑的伪代码和实际的应用程序源代码进行仔细的代码审查,以确定故障开放条件之类的逻辑错误。
如果应用程序执行支持用户伪装功能,应严格控制这种功能,以防止攻击者滥用它获得未授权访问。鉴于这种功能的危险程度,通常有必要从面向公众的应用程序中彻底删除该功能,只对内部管理用户开放该功能,而且他们使用伪装也应接受严格控制与审核。
应对多阶段登录进行严格控制,以防止攻击者破坏登录阶段之间的转换与关系。
有关登录阶段进展和前面验证任务结果的所有数据应保存在服务器端会话对象中,绝不可传送给客户端或由其读取。
禁止用户多次提交一项登录信息;禁止用户修改已经被收集或确认的数据。如果需要在几个阶段使用同一个数据(如用户名),应在第一次收集时将该数据保存在会话变量中,随后从此处引用该数据。
在每一个登录阶段,应首先核实前面的阶段均已顺利完成。如果发现前面的阶段没有完成,应立即将验证尝试标记为恶意尝试。
为防止泄露的是哪个登录阶段失败(攻击者可利用它轮流针对每个阶段发动攻击)的信息,即使用户无法正确完成前面的阶段、即使最初的用户名无效,应用程序也应总是处理完所有的登录阶段。在处理完所有的登录阶段后,应用程序应在最后阶段结束时呈现一条常规“登录失败”消息,并且不提供失败位置的任何信息。
如果在登录过程中需要回答一个随机变化的问题,请确保攻击者无法选择回答问题。
总是采用一个多阶段登录过程,在第一阶段确认用户身份,并在后面的阶段向用户提出随机变化的问题。
如果已向某一用户提出一个特定的问题,将该问题保存在永久性用户资料中,确保每次该用户尝试登录时向其提出相同的问题,直到该用户正确回答这个问题。
如果向某个用户提出一个随机变化的质询,将提出的问题保存在服务器端会话变量而非HTML表单的隐藏字段中,并根据保存的问题核实用户随后提供的答案。
注解
以上详细介绍了设计一个安全验证机制的微妙之处。提出一个随机变化的问题时稍不谨慎就可能给攻击者提供用户名枚举的机会。例如,为防止攻击者选择回答他知道答案的问题,应用程序可能会将该用户提出的最后一个问题保存在用户资料中,并不断提出该问题直到得到正确答案。这样,使用相同用户名多次登录的攻击者就会遇到相同的问题。但是,如果攻击者使用一个无效的用户名进行相同的操作,应用程序处理的方法可能会有所不同:由于没有与无效用户名有关的用户资料,也没有问题被保存起来,因此,应用程序将提出一个不同的问题。攻击者可以利用这种在多次登录尝试中表现出来的行为差异,推断某个特殊用户名的有效性。在一次自定义攻击中,攻击者能够迅速获得大量用户名。
如果应用程序希望防御这种可能性,它必须采取一些预防措施。如果收到使用无效用户名发起的登录尝试,应用程序必须在某个位置记录向这个无效用户名提出的随机问题,并确保随后使用这个用户名登录都会遇到相同的问题。更进一步,应用程序可定期更换到一个不同的问题,模拟不存在的用户已作为正常用户登录,导致提出的下一个问题出现变化。但是,从某种意义上说,应用程序设计者必须做出让步,因为挫败意志如此坚定的攻击者几乎是不可能的。
应用程序使用的各种验证机制不应通过公开的消息,或者通过从应用程序的其他行为进行推断,来揭示关于验证参数的任何信息。攻击者应无法判定是提交的哪个数据造成了问题。
应由单独一个代码组件使用一条常规消息负责响应所有失败的登录尝试。这样做可避免由不同代码路径返回的本应不包含大量信息的消息,因为消息排版方面的差异、不同的HTTP状态码、其他隐藏在HTML中的信息等内容而让攻击者看出差别,从而产生一个细微的漏洞。
如果应用程序实行某种账户锁定以防止蛮力攻击(如6.4.5节所述),应小心处理以防造成信息泄露。例如,如果应用程序透露,由于Y
次失败登录,已将某个特殊的账户冻结X
分钟,这种行为就可被用于枚举有效的用户名。另外,明确公开账户锁定策略标准也使攻击者能够调整任何登录尝试,不顾锁定政策继续猜测密码。为避免用户名枚举,如果从相同浏览器发出一系列失败的登录尝试,应用程序应通过一条常规消息提出警告:如果出现多次登录失败,账户将被冻结,并建议用户稍后再试。可通过使用一个cookie或隐藏字段追踪来自相同浏览器的重复登录失败,从而达到上述目的。(当然,不应使用这种机制实行任何实际的安全控制,仅用于为努力回忆其证书的普通用户提供帮助。)
如果应用程序支持自我注册,那么它能够以两种方式防止这种功能被用于枚举现有用户名。
不允许自我选择用户名,应用程序可为每个新用户建立一个唯一(和无法预测)的用户名,防止应用程序披露表明一个选定的用户名已经存在的信息。
应用程序可以使用电子邮件地址作为用户名。如果是这样,应用程序会在登录过程的第一个阶段要求用户输入他们的电子邮件地址,然后告诉他们等待接收一封电子邮件,按照其中的指示操作。如果电子邮件地址已经被注册,应用程序会在电子邮件中通知用户。如果该地址没有被注册,应用程序会要求用户访问一个唯一的、无法猜测的URL继续注册过程。这样可防止攻击者枚举有效的用户名(除非他们碰巧已经攻破大量电子邮件账户)。
必须对验证功能执行的各种质询采取保护措施,防止攻击者企图使用自动工具响应这些质询。这包括登录机制、修改密码功能和恢复遗忘密码等功能中的质询。
使用无法预测的用户名,同时阻止用户名枚举,给完全盲目的蛮力攻击设置巨大障碍,并要求攻击者在实施攻击前已经通过某种方式发现一个或几个特殊的用户名。
一些对安全性要求极高的应用程序(如电子银行)在检测到少数几次(如3次)登录失败后应立即禁用该账户,并要求账户所有者采取各种非常规步骤重新激活该账户,如给呼叫中心拨打电话并回答一系列安全问题。这种策略的缺点在于:它允许攻击者通过重复禁用合法用户的账户向他们发动拒绝服务攻击,因而增加了提供账户恢复服务的成本。一种更加均衡的策略适用于非常注重安全的应用程序,即在检测到少数几次(如3次)登录失败后将该账户冻结一段时间(如30分钟)。这种策略可有效阻止密码猜测攻击,同时可降低拒绝服务攻击风险,减轻呼叫中心的工作负担。
如果采用临时冻结账户的策略,应采取措施确保这种策略的效率。
为防止信息泄露导致用户名枚举,应用程序绝不能透露任何账户冻结信息。相反,应用程序应对一系列即使是使用无效用户名发起的失败登录做出响应,通过一条常规消息提出警告:如果出现多次登录失败,账户将被冻结,建议用户稍后再试(如前文所述)。
应用程序不应向用户透露账户锁定标准。只要告诉合法用户“稍后再试”并不会显著降低服务质量。但告知攻击者应用程序到底能够容忍多少次失败的登录尝试、账户冻结期有多长,就会让他们对任何登录尝试进行调整,不顾账户锁定策略而继续猜测密码。
如果一个账户被冻结,那么应用程序不用检查用户证书,直接就可以拒绝该账户的登录尝试。因为一些应用程序在冻结期继续完全处理登录尝试,并且在提交有效证书时返回一条差异并不明显(或者差异比较明显)的消息,因此尽管应用程序执行账户冻结策略,攻击者仍然能够利用这种行为实施彻底有效的蛮力攻击。
账户锁定之类的常规应对措施对防御一种极其有效的蛮力攻击并没有帮助,即遍历大量枚举出的用户名,检查单独一个脆弱密码,如password。例如,如果5次登录失败就会触发账户冻结,这意味着攻击者能够对每个账户尝试使用4个不同的密码,而不会引起任何中断。如果一个应用程序使用许多脆弱密码,使用上述攻击手段的攻击者就能够攻破许多账户。
当然,如果验证机制其他区域的设计安全可靠,这种攻击的效率就会显著降低。如果攻击者无法枚举或有效预测出用户名,他就需要实施蛮力攻击以猜测用户名,其攻击速度也随之减慢。如果应用程序执行了严格的密码强度要求,攻击者更没有可能选择某个应用程序用户已经选择的密码进行测试。
除以上控制外,应用程序还可以在每个可能成为蛮力攻击目标的页面(见图6-9)使用CAPTCHA [2] (全自动区分人类和计算机的图灵测试)质询,专门防御这种攻击。实际上,这种措施可防止攻击者向任何应用程序页面自动提交数据,从而阻止其手动实施各种密码猜测攻击。实际上,人们已经对CAPTCHA技术进行了大量的研究。有些时候,针对这种技术的自动攻击已经能够取得相当的成效。此外,一些攻击者甚至发起了破解CAPTCHA的竞赛,利用不知情的公众人物作为标靶帮助攻击者实施攻击。但是,即使一类特殊的质询无法完全生效,它仍然可使大多数随意的攻击者停止攻击行动,转而寻找并不使用这种技术的应用程序。
图6-9 旨在阻止自动攻击的CAPTCHA控件
提示
攻击者在攻击一个使用CPATCHA控件阻止自动攻击的应用程序时一定会仔细检查图像页面的HTML源代码。我们曾遇到过许多实例,其中谜题的答案以文字形式出现在图像标签的ALT属性或一个隐藏表单字段中,这使精明的攻击者不必解开谜题就可以解除应用程序执行的保护。
应用程序应始终执行密码修改功能,允许定期使用的密码到期终止(如有必要)并允许用户修改密码(不管他们出于任何原因希望修改密码)。作为一种关键的安全机制,我们必须精心设计这项功能以防止滥用。
不应以任何方式直接提供用户名,也不能通过隐藏表单字段或cookie提供用户名。用户企图修改他人密码的行为属非法行为。
作为一项高级防御措施,应用程序应对密码修改功能加以保护,防止攻击者通过其他安全缺陷,如会话劫持漏洞、跨站点脚本,甚至是无人看管的终端获得未授权访问。为达到这种目的,应要求用户重新输入现有密码。
为防止错误,新密码应输入两次。应用程序应首先比较“新密码”与“确认新密码”字段,看它们是否匹配,如果不相匹配,返回一条详细的错误消息。
该功能应阻止可能针对主要登录机制的各种攻击:应使用一条常规错误消息告知用户现有证书中出现的任何错误;如果修改密码的尝试出现少数几次失败,应临时冻结该功能。
应使用非常规方式(如通过电子邮件)通知用户其密码已被修改,但通知消息中不得包含用户的旧证书或新证书。
当用户遗忘密码时,许多安全性至关重要的应用程序(如电子银行)通过非常规方式完成账户恢复:用户必须给呼叫中心打电话并回答一系列安全问题;新证书或重新激活代码也以非常规方式(通过传统的邮件)送往用户注册的家庭住址。绝大多数应用程序并不需要这种程度的安全保护,只需使用自动恢复功能即可。
精心设计的密码恢复机制需要防止账户被未授权方攻破,避免给合法用户造成任何使用中断。
绝对不要使用密码“暗示”之类的特性,因为攻击者可利用明显的暗示向账户发动攻击。
通过电子邮件给用户发送一个唯一的、具有时间限制的、无法猜测的一次性恢复URL是帮助用户重新控制账户的最佳自动化解决方案。这封电子邮件应送至用户在注册阶段提供的地址中。用户访问该URL即可设置新密码。之后,应用程序会向用户送出另一封电子邮件,说明密码已被修改。为防止攻击者通过不断请求密码重新激活电子邮件而向用户发动拒绝服务攻击,在证书得到修改前,用户原有证书应保持有效。
为进一步防止未授权访问,应用程序可能会向用户提出一个次要质询,用户必须在使用密码重设功能前完成该质询。设计质询时应小心谨慎,确保不会引入新的漏洞。
应用程序应在注册阶段规定:质询应对每一名用户提出同一个或同一组问题。如果用户提供自己的质询,可能其中会有一些非常易于受到攻击,这也使攻击者能够通过确定那些自行设定质询的用户枚举出有效的账户。
质询响应必须具有足够的随机性,确保攻击者无法轻易猜测出来。例如,询问用户就读的小学名称就优于询问他们最喜欢的颜色。
为防止蛮力攻击,如果多次尝试完成质询都以失败告终,应临时冻结相关账户。
如果质询没有得到正确响应,应用程序不应泄露任何相关信息,如用户名的有效性、
账户冻结等。
成功完成质询后,应继续完成上文描述的处理过程,即向用户注册的电子邮件地址发送一封包含重新激活URL的电子邮件。无论在什么情况下,应用程序都不得透露用户遗忘的密码或简单将用户放入一个通过验证的会话中。此外,最好不要直接进入密码重设功能,因为与初始密码相比,攻击者通常更容易猜测出账户恢复质询的响应,因此应用程序不应依赖它对用户进行验证。
应用程序应在日志中记录所有与验证有关的事件,包括登录、退出、密码修改、密码重设、账户冻结与账户恢复。应在适当的地方记录所有失败与成功的登录尝试。日志中应包含一切相关细节(如用户名和IP地址),但不得泄露任何安全机密(如密码)。应用程序应为日志提供强有力的保护以防止未授权访问,因为它们是信息泄露的主要源头。
应用程序的实时警报与入侵防御功能应对验证过程中的异常事件进行处理。例如,该功能应向应用程序管理员通报所有蛮力攻击模式,便于他们采取适当的防御与攻击措施。
应以非常规方式向用户通报任何重大的安全事件。例如,用户修改密码后,应用程序应向他注册的电子邮件地址发送一封邮件。
应以非常规方式向用户通报经常发生的安全事件。例如,用户成功登录后,应用程序应向用户通报上次登录的时间与来源IP/域,以及从那以后进行的无效登录尝试的次数。如果用户获悉其账户正遭受密码猜测攻击,他就更有可能会经常修改密码,并设置一个安全性高的密码。