一、目标:恢复或重置密码
每个有密码的程序都会碰到用户忘记密码的情况,现今大多数程序都通过E-mail的回馈机制让用户恢复或者重置密码。这个解决方案有一个前提,这个服务有一个前提,就是这个用户能够访问他在注册时留下的邮箱。
二、反模式:使用明文存储密码
在这种恢复密码的解决方案中,很常见的一个错误是允许用户申请系统发送一封带有明文密码的邮件。这是数据库设计上一个可怕的漏洞,并且会导致一系列安全问题,可能会使得未取得授权的人获得系统访问权限。
1、存储密码
首先我们设计一张表如下:
类似于这张表,我们插入一条记录的SQL语句如下:
INSERT INTO Account (AccountId,AccountName,Email,Password) VALUES(100,'admin','admin@126.com','123456')
使用明文存储密码或者使用明文在网络上传递密码是不安全的。如果攻击者能够截获你用来插入密码的SQL语句,他们就能直接获得密码。在更新密码或者验证用户是否输入正确的密码时这么做,也会导致同样的密码,黑客可以通过下面这几种方法盗取用户密码:
- 在客户端和服务器端数据库交互的网络线路上截获数据包。这样做比你想象的要容易得多。很多软件能够做到这一点,例如:Wireshark。
- 在数据库服务器上搜索SQL的查询日志。要这么做的前提是,黑客能够访问到数据库所在的服务器,假设他们真的能登录上服务器上,他们就可以查看那些带有SQL语句的数据库执行日志。
- 从服务器或者备份介质上读取数据库备份文件内的数据。你的备份文件妥善保管了吗?你在回收或者丢弃备份设备之前彻底清理干净里面的数据了吗?
2、验证密码
大多数时候,我所看到的认证查询是将AccountId和Password两列同时放在WHERE子句里进行匹配查找:
SELECT * FROM Account WHERE AccountName = 'admin' AND Password = '123456789'
当账号不存在或者用户输入的密码不正确时,整个查询返回空。你的程序无法区别是账号不正确还是密码不正确。最好是使用一个能区分这两种情况的查询方法,那样就可以根据错误合适地选择处理方式了。
比如,当发现在短时间内同一个账号有很多失败的登陆请求时,你可能会想暂时冻结这个账号,因为这可能是一次恶意攻击。然而,所面临的问题是你使用的查询语句没办法区分到底是用户名输错了还是密码输错了。
3、在E-mail中发送密码
由于密码在数据库中是以明文形式存储的,你可以很简单地在程序中获取密码:
SELECT AccountName,Email,Password FROM Account WHERE AccountId = 100
随后你的程序就可以根据用户的请求将密码发送到用户的邮箱里,内容如下:
From : xxx.com To : admin@126.com Subject : Password Request 你名为"peter"的账户请求密码提醒 你的密码是"123456" 点击链接登录你的账户: http://www.xxxxx.com/login
将明文密码通过邮件发送是非常严重的安全隐患。E-mail可能会被黑客劫持、记录或者使用多种方式存储。就算使用安全协议查看邮件,收发邮件的服务由值得信赖的管理员维护,也不一定能够保证安全。由于E-mail的收发都需要经由网络层传输、数据可能会在其他路由节点上被截获。同时,如果用户的E-mail被破获,那么用户的账号密码也随之沦陷。
三、识别反模式
任何能够恢复你的密码或者将你的密码通过邮件以明文或可逆转加密的格式发给你的程序,都必然犯了本文的反模式。如果你的程序可以通过一个合法的方式获得用户的明文密码,那么黑客也同样可以。
1、合理使用反模式
并不是所有的程序都有被攻击的风险,也不是所有的程序都有敏感的需要保护的信息。比如说,一个可能只有靠几个可靠的内部人员访问的内部程序,认证机制就可能足够了。在那些非正式的环境中,一个简单的登录框就已经足够了。额外简历一个强验证系统可能并不合理。
四、解决方案:先哈希后存储
1、理解哈希函数
使用哈希函数对原始密码进行加密,哈希是指将输入字符转换为另一个新的、不可识别的字符串函数。使用哈希函数后,连原始输入串的长度也变得难以猜测了,因为哈希函数返回的字符串的长度是固定的。
目前比较可靠的是SHA-256算法。
MD5是零一个流行的哈希函数,产生128位哈希串。MD5也被证明是弱加密,因此你最好不要用它来加密密码。
2、在SQL中使用哈希
下面对Account表进行重定义。SHA-256总是一个64字节的字符串,因此这一类型可以改成是固定长度CHAR。
在SQL Server中,提供了一个方法,让你可以随意调用常用的哈希函数:hashbytes,由于SHA-1在2010年之前是美国国家标准和技术协会的标准,在2010年之后才逐步取消SHA-1的标准,随之跟上的是SHA-225、SHA-256、SHA-384、SHA-512。所以,在我的SQL Server2008中,并没有这一支持。本处仅仅以SHA-1,MD5作为示例(后面也是):
--SHA1 SELECT hashbytes('sha1','123') --MD5 SELECT hashbytes('md5','123')
3、给哈希加料
如果你使用哈希串替代了原始代码,然后攻击者获得了对数据库的访问权限(他翻了你的额垃圾桶,找到了被丢弃的备份CD),他仍旧可以通过试错法获取用户密码。要猜出密码可能会花很长时间,但他可以预先准备自己的数据库-存储可能的密码和对应的哈希串,然后和从你的数据库找到的哈希串进行比较。只要有一个用户选择了字典中存在的单词,攻击者就能够很轻易地通过搜索两边的哈希值来找到对应的密码原文。他甚至可以直接用SQL来做这件事。
假设它有一张字典表,里面存储了很多密码字符串与加密后的字符串:
CREATE TABLE DictionaryHashes( password VARCHAR(100), password_hash CHAR(64) );
查询语句:
SELECT a.AccountName,h.Password FROM Account AS a JOIN DictionaryHashes AS h ON a.password_hash = h.password_hasn
防御这种"字典攻击"的一种方法是给你的密码加密表达式加点佐料。具体方法是在将用户密码传入哈希函数进行加密之前,将其和一个无意义的串拼接在一起,即使用户选择了一个在字典中存在的单词作为密码,对加料密码进行哈希得到的串是不太会出现在攻击者的哈希数据库中的,你可以发现增加了随机串得到的哈希值和原始值是不一样的:
每个密码都应该配上不同的随机串,这样攻击者就必须为每个密码都创建一个新的哈希字典。然后他就会回到起点上,因为破解数据库中的密码所花的时间和靠猜达到目的的时间差多。
佐料的合理长度应该是8个字节。你需要为每个密码随机生成佐料。这样防止黑客破解了一个就几乎等于破解了全部。
从哈希值恢复密码的一种更优雅的技术叫做彩虹表,它的性能让人吃惊,但引入随机字符串也能防御这种技术。
4、在SQL中隐藏密码
现在,你在存储密码之前已经有了一个强哈希函数来对密码进行加密,并且使用了随机字符串来阻止字典攻击,你可能认为这样已经足够安全。但是,密码还是会在SQL表达式中以明文中出现,这意味着如果攻击者截获了网络通信的数据包,或者记录了相关的查询语句的日志给到了错误人手里,密码就泄露了。
只要不将明文密码放到SQL查询语句中,就能避免这种类型的泄露。你所需要做的就是在程序代码中生成密码的哈希串,然后在SQL查询中使用哈希串。这么做的好处是,几时攻击者截获了数据包,它也没有办法将哈希反转成他所需要的密码。
也就是说,在C#中加密之后,再在SQL语句中执行。
在网络程序中,还有另一个地方是攻击者有机会截获网络数据包的:在用户浏览器和网站服务器之间,当用户提交了一个登录表单,浏览器将用户的密码以明文形式发送到服务器端,随后服务器端才能使用这个密码进行之前所介绍的哈希运算。你可以通过在用户的浏览器发送表单数据之前就进行哈希运算来解决这个问题。但是这个方案有一个问题,就是你需要在进行正确的哈希运算之前,还要通过别的途径获得和这个密码相关联的佐料,这部分最好放到服务器端进行。或者在浏览器向服务器端提交密码表单时,使用安全的HTTP(https)连接。
浏览器端加密一次,服务器端添加佐料再加密一次这个方法也不错。
5、重置密码,而非恢复密码
现在密码已经以一个更安全的方法存储了,但是还有一个问题需要解决,就是帮助那个忘记密码的用户找回密码。由于现在数据库中存着的密码是哈希串而不是原始密码,你已经无法恢复他们的密码了。你没有办法比一个攻击者更快速地反转一个哈希值。但是可以允许用户用别的途径获得访问权限。
方案1
当用户忘记他们的密码,请求帮助的时候,程序发送一封带有临时生成密码的邮件给用户,而不是直接发给它自己的密码。
邮件内容如下:
From:admin To:admin@126.com Subject:password reset 你要求重置你账户的密码。 你的临时密码是"admin123", 1个小时之后,这个密码将不能使用, 点击如下链接直接登录你的账号并设置你的密码: http://www.xxx.com/login
方案2
在数据库记录下这个请求,并且为其分配一个唯一的令牌作为标识,而不是发送带有新密码的邮件
CREATE TABLE PasswordResetRequest( token CHAR(32) PRIMARY KEY, account_id int expiration TIMESTAMP NOT NULL, FOREIGHKEY (account_id) REFERENCES Account(account_id) ) SET @token = MD5('admin' || CURRENT_TIMESTAMP); INSERT INTO PasswordResetRequest(token,account_id,expiration) VALUES(@token,123,CURRENT_TIMESTAMP + INTERVAL 1 HOUR)
随后,你可以在E-mail中包含这个令牌,你也可以使用别的途径发送这个令牌,比如短信,只要能将消息送达到这个请求重置密码的账号所有者即可。使用这种方法,如果一个陌生人非法地请求一次密码重置,系统指挥发送E-mail给这个账号的实际拥有者。
密码重置页面临时链接邮件:
From:admin To:admin@126.com Subject:password reset 你要求重置你账户的密码。 在1个小时内点击下面连接来改变密码 1个小时之后,这个链接将不能使用 http://www.xxx.com/reset_password?token=f5kajkjkdsjfs1dnjpoqw
当程序收到一个重置密码页面发来的请求,令牌的值必须存在于密码重置请求表中,并且该执行的过去时间点必须是一个将来的时间点而不是过去的时间点,同时,该行的Account_id引用Account表,因此,这个令牌被约束为只能重置一个指定的账号。
当然,如果其他人访问了这个页面,也会造成问题。可以通过一些简单的方法来减小风险,比如这个特殊页面的有效期非常短,并且这个页面上不会显示哪个账号的密码要求被重置。