后端应该如何安全地储存用户数据?

后端应该如何安全地储存用户数据?

今天AcFun数据库泄露的消息一大早就沸沸扬扬地传开了,所幸从AcFun的公告里可以看出泄露的用户密码均经过「加密」(实际上指哈希),所以我们仍然是安全的。

从泄露出的部分信息可以看到,在存储密码这件事上,AcFun并没有犯什么低级错误,加之道歉诚恳,很多人表示「选择原谅」。


合格的后端开发者和运维人员需要考虑每个接口的安全性、每个可被公网访问到的服务器的安全性,保证不要犯错。但是我们还需要考虑,如何在已经犯错的情况下把损失降低到最小。在做数据的存储方案时,我们需要假设储存的数据已经被泄露出去了,如用户密码这种隐私数据的存储就是一个重点。

讲密码的存储方案前,先要记住三条前提:

  1. 用户喜欢到处使用一样的密码
  2. 用户喜欢使用简单好记的密码
  3. 世界上没有绝对的安全,但当攻击成本远高于收益时,整个系统达到相对安全

明文存储?

即使仅从道德的角度来说,后端也不应该以明文形式存储用户密码。

2011年CSDN密码泄露事件和2016年的网易52G事件出现后,比他们数据库被盗这件事更令人震惊的是,他们泄露的数据库里的密码居然全部都是明文。

明文存储的方法将安全性全部下放给了运维人员、开发人员和管理人员:

  • 从运维层面看,任何操作系统漏洞、基础工具漏洞的发生,都会导致密码泄露
  • 从开发层面看,任何代码逻辑有漏洞、任何依赖库的漏洞都可能导致密码泄露
  • 从管理层面看,任何一个有权限读取数据库的人,都能看到所有用户的密码

由于前提1(大家总是到处使用一样的密码),所以明文密码泄露之后,再使用撞库攻击,还能得到其他网站的用户密码,对用户造成更严重的危害。比如2014年的12306密码泄露事件里,12306并没有犯错,只是黑客收集了很多其他网站的明文账号密码,再去12306批量尝试登陆,就可以得到大量用户的12306信息。

所以,后端不应该以任何明文或可以转换回明文(如可逆的加密)的形式储存密码。由于储存的信息并不是明文,所以大多数网站的「找回密码」功能并不能真的告诉你密码,只能让你重新设置一次。如果你发现一个网站的「找回密码」功能真的找回了你的密码,那就要当心你密码的安全性了。

做个哈希?

要保存密码的某种可被验证的形式,又要让保存的数据「不可逆」,首先能想到的就是哈希了。

十年前,大多数网站保存的都是经过一次MD5哈希的密码。哈希保存的思路很简单:用户注册时,把他的密码做一次MD5运算储存起来;用户登录时,把他输入的密码做一次MD5运算,再验证是否和数据库里储存的一致。

在MD5被证明不够安全以后,大家又开始选择其他的哈希算法如sha256等。但除了哈希算法本身的安全性外,这种做法的缺陷很快暴露,就是应付不了彩虹表的攻击方式。

彩虹表就是把简单的数字密码组合(和各种常见密码)的哈希先尽可能的计算出来,这些明文和哈希结果的对应关系就是一张彩虹表。由于前提2(大家喜欢使用简单好记的密码),所以试着计算出一个常用范围内的所有字母组合的哈希的彩虹表,可以破解绝大多数人的密码。当彩虹表足够大时,这种存储方式实际上与明文无异。

下图是cmd5这个网站已经破解的明文范围。

在这种储存方式下,一次运算出的彩虹表可以被使用无数次,均摊到每个网站的每个用户的成本是极低的,所以至今仍然有很多服务器在孜孜不倦地运算着md5哈希的彩虹表。这张彩虹表也在大家的努力下变得越来越大。

加盐哈希?

加盐哈希是目前业界最常见的做法。

加盐哈希的步骤如下:

  • 用户注册时,给他随机生成一段字符串,这段字符串就是(Salt)
  • 把用户注册输入的密码和盐拼接在一起,叫做加盐密码
  • 对加盐密码进行哈希,并把结果和盐都储存起来

在登陆时,先取出盐,再同样进行拼接、计算哈希,就能判断密码的合法性。

加盐哈希的做法,既保证了储存数据的不可逆,又防止了上一章的彩虹表攻击方式。这种方式下,黑客拿到数据库后,如果再要用遍历所有常用的密码组合的方式做彩虹表,那他需要对所有常用密码+盐值进行哈希运算。而每个用户的盐值都不相同,之前彩虹表的「一次运算无数次使用」变成了「一次运算一次使用」。这样的成本是难以接受的,由于前提3(攻击成本远高于收益,系统达到相对安全),所以这是一个比较安全的做法。

除了密码,还有别的信息

除了密码以外,手机号等信息也是重要的隐私数据。但手机号与密码不同:对于后端来说,永远不知道密码的明文也不会对业务逻辑造成影响;而后端可能需要明文的手机号,在一些情况下给用户发送短信。

对于手机号这种信息,只能用相对安全的做法,即先对手机号进行对称加密,再将加密结果储存在数据库里;使用时再用密钥解开。

这时密钥不应该被保存在数据库里。如果数据库被拖库,那么些数据的安全性与明文无异。通常会将密钥以环境变量的形式放在服务器上。这时除非网站在被拖库的情况下同时被拿到服务器权限,否则手机号的明文就不会被泄露出去。

不幸的是,从今天AcFun泄露数据来看,A站对手机号应该也做了对称加密存储,但它就是在被拖库的情况下同时被拿到了服务器权限,所以手机号就以明文形式展示了出来。

除了数据库,还有别的地方

就在上个月,2018年的5月4日,twitter发布了一则公告,称他们从来不会把密码的明文储存下来,但他们自己发现了一个漏洞:密码被储存在请求日志里了。

在后端项目生产环境里,总是会打印并收集各种各样的日志。日志可以方便统计数据、在出错的时候回溯、调试bug。但是我们不应该总是打印和收集全部请求的正文内容,因为请求里可能包含密码等敏感信息。当日志被泄露时,用户数据同样会以明文形式直接泄露出去。