为密码重置接口实现一次性令牌:从哈希存储到并发重放测试

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
321
文章
0
粉丝
安全开发1 14字数 1659阅读5分31秒阅读模式
AI智能摘要
给令牌设了过期时间就安全吗?若两次请求在过期前都读到同一条有效记录,而改密与删除令牌分两步,两次都可能成功。可行路径是:数据库只存令牌的 SHA-256 摘要并建唯一索引,邮件链接的 GET 只展示表单不消耗令牌,真正消费放在提交新密码的 POST 事务中,用 FOR UPDATE 锁行后改密并删除。如何用两个独立连接制造并发重放,验证恰好一次成功?
— AI 生成的文章内容摘要

“密码重置令牌设置了过期时间,就足够安全了”是一个容易漏掉并发条件的假设。过期时间只限制令牌何时不能再用;如果两次请求都在过期前读到同一条有效记录,而密码更新和令牌删除又是分开的操作,两次请求仍可能各自成功。要实现一次性令牌,关键不是缩短有效期,而是让校验、修改密码、消耗令牌形成不可拆开的事务。

准备:定义最小接口和数据

以两个接口为例:

为密码重置接口实现一次性令牌:从哈希存储到并发重放测试
  • POST /password-resets 接收邮箱。无论账号是否存在,都返回相同的通用答复;存在账号时,在后台生成重置链接并发送到已登记邮箱。
  • POST /password-resets/complete 接收令牌和新密码。成功时修改密码,并使该令牌不可再次使用。

服务端用密码学安全随机数生成令牌,例如生成 32 字节随机值再编码为适合放入链接的形式。原始值只交给投递流程和用户,不存入数据库,也不写入应用日志。数据库保存令牌的 SHA-256 摘要、所属账号和到期时间,并为摘要建立唯一索引。这里使用普通摘要的前提是原始令牌具有足够高的随机性;用户密码仍须使用专门的密码哈希算法,不能照搬令牌的存储方式。

邮件链接的 GET 请求只展示重置表单,不消耗令牌;真正的消费发生在提交新密码的 POST 请求中。这样也能避免邮件安全扫描器预先打开链接就使令牌失效。

执行:把消费放进数据库事务

下面是适用于 PostgreSQL、以数据库保存令牌和会话的示意伪代码。新密码的策略校验和密码哈希计算放在事务外,减少持锁时间;是否允许重置只能在事务内决定。

digest = SHA256(提交的原始令牌)
new_hash = hash_password(新密码)

BEGIN

row = SELECT user_id, expires_at
      FROM reset_tokens
      WHERE token_digest = digest
      FOR UPDATE

if row 不存在:
    ROLLBACK
    返回“链接无效或已过期”

if row.expires_at <= 数据库当前时间:
    ROLLBACK
    返回“链接无效或已过期”

UPDATE users SET password_hash = new_hash WHERE id = row.user_id
DELETE FROM reset_tokens WHERE token_digest = digest
COMMIT

返回成功

FOR UPDATE 锁住令牌行。两次请求使用同一令牌时,后到的事务必须等待先到的事务结束;先到的事务提交删除后,在 PostgreSQL 的常用 READ COMMITTED 隔离级别下,等待者不能再凭那条已删除的记录完成重置。密码更新与删除处于同一事务,任一步失败都应回滚。实现时还要检查数据库错误和受影响行数,不能把“删除了零行”当作成功。

相反,“先查询有效令牌→更新密码→删除令牌”若没有贯穿全程的行锁和事务,就留出了竞态窗口:请求 A、B 都完成查询后,A 删除记录并不撤销 B 已作出的“有效”判断。单独给 DELETE 加事务,也修不好此前在事务外完成的判断。

若产品要求“成功重置后,立即撤销该账号所有其他重置链接”,签发、消费和批量撤销还需约定一致的账号级并发控制;上述最小示例只保证同一令牌的一次性消费。

判断结果:把竞态变成回归测试

在授权测试环境中,先创建测试账号并取得测试投递通道中的原始令牌,再覆盖以下路径:

场景操作通过标准
顺序重放同一令牌提交成功后再次提交第二次失败,密码不再改变
并发重放用同一令牌、两个不同的新密码同时提交恰好一次成功;最终密码只对应成功请求
过期令牌在测试数据库中设置已过期时间后提交密码不变,令牌不被视为有效
账号探测分别用存在和不存在的邮箱申请重置对外答复不泄露账号是否存在,且不存在账号时不发送链接

并发测试应使用两个独立数据库连接,在请求入口设置同步点并重复运行,记录每次响应、最终密码和令牌记录。只让两个 HTTP 请求“差不多同时发出”不能稳定覆盖竞态;验证旧实现时,可在测试专用代码中于“查询成功、删除之前”设置同步点,证明两次请求都能越过检查。修复后移除该测试钩子,再做并发回归。测试也要确认异常回滚时不会出现“密码已改、令牌仍可用”。

申请接口的通用文案只是账号探测防护的一部分,还需检查响应状态、可观察的耗时差异和发送频率,并对申请与消费接口设置适当的限流。OWASP 的忘记密码实践指南也将令牌安全性、使用后失效和账号枚举列为该流程需要考虑的问题。

上线:明确会话影响与回退边界

密码重置是否注销现有会话必须由产品和安全策略明确决定,不能让实现细节碰运气。若要求立即失效,可以在同一数据库事务中提高账号的会话版本,并让每次会话鉴权检查该版本;仅在响应里提示“已退出其他设备”,却由不可靠的异步任务稍后撤销会话,不应算作立即生效。上线前验证旧会话、当前会话和新登录各自的预期行为。

先对测试账号和小范围流量验证成功率、事务冲突、邮件投递及“无效或过期”响应的变化。若新流程异常,优先暂停自助重置并转入经过身份核验的人工恢复,同时保留诊断日志;不要回退到可重复使用令牌的旧消费逻辑,也不要恢复已消耗的令牌。修复后重新签发新令牌,再运行顺序重放、并发重放和会话回归测试。一次性令牌解决的是重置凭据的重放问题,并不能代替邮箱安全、限流、会话管理和异常监测。

 
枫少@KillBoy
    • 墨剑行
      墨剑行 1

      邮件扫描器预读确实是个大坑,很多人踩过

    匿名

    发表评论

    匿名网友

    拖动滑块以完成验证