web 安全之 - 关于重置密码链接的几个安全问题

前言

最近一直有在处理一些安全问题,其中有几个是跟重置密码链接相关的安全问题, 所以就想谈一谈关于重置密码链接的几个安全问题,包括以下几个情况:

  1. 刷忘记密码接口使其一直发送邮件
  2. 第三方站点的 referer 头部泄露重置密码链接
  3. 发送新的重置密码链接的时候, 并没有将旧的重置密码链接设置为无效
  4. 重置密码链接用过之后并没有马上设置为无效
  5. 登录密码长度没有限制
  6. 重置密码成功,其他端的 session 就要全部设置为过期。 客户端要重新进行登录
  7. 修改邮箱的时候,要将未失效的重置密码邮件都设置为无效
  8. 重置密码的校验接口,要做速率限制

因此我们在设计这个功能的时候,一定要严防以上情况。

重置密码流程

重置密码的流程大部分分为两种,一种是通过点击邮箱连接来重置,一种是通过输入短信验证码来重置。 我们主要讲的是第一种,也就是通过点击邮箱的充值密码连接来重置。 简单的来说,主要是以下步骤:

  1. 用户在登录界面,点击 忘记密码 按钮
  2. 接下来到一个忘记密码的落地页,然后输入忘记的那个账号的邮箱,点击确认。
  3. 服务端判断这个邮箱是否是有效用户账号,如果存在的话,就发送一封带 token 的链接到这个邮箱中 (一般这个 token 是有时效性的,比如半小时过期,一旦用过也会过期)
  4. 用户到这个邮箱中,点击这个连接,到达重置密码的落地页 (这时候服务端会校验 token 的合法性),然后输入新的密码,并且输入邮箱,点击确认
  5. 服务端校验邮箱是否一致,密码是否合理(不能弱密码),最后重置成功。

这个是正常流程,普通用户肯定是没问题的,但是这个流程有暴露了几个安全问题。

1. 刷忘记密码的接口,使其一直发邮件

一般忘记密码的接口是没有带上 csrf token 的, 所以很容易被攻击者用脚本刷, 让其一直发送垃圾邮件, 不仅会骚扰用户, 而且发邮件还会让费用增加。 所以这个接口可以做以下防范:

  1. 针对不同用户的请求频率要做防刷限制,可以根据 ip, 也可以是别的,比如输入验证码之类的, 比如我们项目就是通过接入 google 验证码来做防刷限制: 使用"Google reCaptcha"来防止站点接口被刷
  2. 如果是针对同一个用户的多次请求,可以判断是否已经有发送链接到邮箱中,并且判断这个连接还没有被激活的话,那么就不需要重新发送,但是可以将重置密码的有效期再重置一下

2. 第三方站点的 referer 头部泄露重置密码链接

关于这个问题,我们要注意如果在重置密码这个页面,如果有引入第三方统计的话,要防止请求第三方统计 js 或者接口的时候, referer 头部会将整个页面的 url 带过去,从而导致 token 暴露给第三方, 所以我们最好要在这个页面禁止掉 referer 头部, 具体防范可以看这个 web 安全之 - 页面禁用 referer(第三方站点的 referer 头部泄露重置密码链接), 这边写的很详细。

3. 发送新的重置密码链接的时候, 并没有将旧的重置密码链接设置为无效

这个问题也需要注意, 有时候用户会连续点击,如果我们的机制是有发送两封不同令牌的有效链接到邮箱的话,那么就要每次生成新的令牌的链接的时候,就要把这个用户的之前的有效令牌的链接全部设置为无效。

简单的来说,就是永远只有最新的那一封是有效的。

4. 重置密码链接用过之后并没有马上设置为无效

这个比较好理解,一般都有设置,就是有效链接使用过之后,要马上设置为无效。

5. 登录密码长度没有限制

一般我们对密码的最短长度和复杂度都有一定的要求,但是密码的最大长度也应该有限制。 不然通过 post 方式,请求了一大串的字符串,会对服务器的资源造成消耗。

尤其是那些密码直接存明文的,直接就数据库字段就溢出了。 就算是存密文,比如通过 md5 加盐变成固定的 32 位,但是如果字符串太大,几十M 乃至 上百M,那么 md5 加密就要很久,甚至会耗尽服务器的资源。 所以密码的最大长度一定要有限制,国外的网站一般就是设置为 48 个。

前端的话直接在 type=password 的 input 组件加上 maxLength=48 的属性即可。

6. 重置密码成功,其他端的 session 就要全部设置为过期。 客户端要重新进行登录

既然密码修改了,为了安全, 那么之前的所有的 session 都应该过期。 客户端要重新登录。

7. 修改邮箱的时候,要将未失效的重置密码邮件都设置为无效

这个细节也要稍微注意一下, 因为之前也有白帽子反馈了这个问题:

  1. 创建一个账号,邮箱 A。 然后点击 忘记密码, 这时候服务端就会将 重置密码 的邮件发送到 A 邮箱
  2. 登录这个账号,然后修改邮箱为 B
  3. 继续点击 A 邮箱中的 重置密码邮件,成功更改密码

从安全性方面来说,确实邮箱如果换了之后,旧邮箱里面的链接如果涉及到业务的话,确实应该不能生效。 所以要记得在修改邮箱的时候,一定要将未失效的重置密码邮件都设置为无效。

8. 重置密码的校验接口,要做速率限制

有时候黑客如果想暴力枚举我们的重置密码的 token 的,可以用脚本一直刷重置密码的校验接口。 所以一定要对这个接口做速率限制。 比如一分钟之内,同一个 ip,只能接受 5 次请求等等。 必要时甚至可以通过接入 google 验证码来做防刷限制: 使用"Google reCaptcha"来防止站点接口被刷