您认证系统中的隐藏漏洞:深入探讨 Timing Attacks、IP Spoofing 和 Race Conditions
Source: Dev.to
引言
认证是应用安全的基石。然而,许多开发者——即使是有经验的开发者——也会忽视一些细微的漏洞,这些漏洞可能泄露用户数据、导致账户枚举,或让攻击者绕过速率限制。过去几个月里,我一直在加固一个认证服务,发现了三个在生产系统中出人意料地常见的关键漏洞。
本文将逐一介绍这些漏洞,解释它们为何危险,并展示如何修复。无论你是在构建自己的认证系统,还是在评估第三方方案,了解这些问题都至关重要。
漏洞 #1:密码验证的时序攻击
问题
攻击者可以通过发送不同密码的登录请求并测量响应时间来猜测用户密码。如果服务器在用户名存在时(即使密码错误)响应时间更长,攻击者就能得知该账户是否存在——这就是经典的时序攻击。
发生了什么
当用户不存在时,函数会立即返回;当用户存在但密码错误时,它会先执行耗时的密码验证。这个时序差异泄露了信息。
为什么重要
- 账户枚举 – 攻击者可以确定哪些电子邮件地址已注册。
- 用户隐私 – 揭示账户是否存在。
- 有针对性的攻击 – 攻击者可以集中攻击已知账户。
修复方案
通过始终执行密码验证(即使是对不存在的用户),实现常量时间验证。
// SECURE CODE
const user = await findUserByEmail(email);
const DUMMY_HASH = '$argon2id$v=19$m=65536,t=3,p=4$...'; // 预先计算的虚拟哈希
const DUMMY_PASSWORD = 'invalid';
// 始终并行执行两次验证
const [, realResult] = await Promise.all([
verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // 虚拟验证(始终失败)
user && user.passwordHash
? verifyPassword(user.passwordHash, input.password)
: verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // 没有用户时使用虚拟验证
]);
if (!user || !user.passwordHash || !realResult) {
throw new AuthError('Invalid credentials');
}
关键改进
- 两条路径耗时相近(常量时间执行)。
- 不会泄露用户是否存在的信息。
- 并行执行保持了性能。
漏洞 #2:速率限制中的 IP 欺骗
问题
速率限制对于防止暴力破解至关重要,但许多实现直接信任 X‑Forwarded‑For 头部,而该头部可以被攻击者伪造。
// VULNERABLE CODE
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
// Use IP for rate limiting
为什么重要
- 绕过速率限制 – 攻击者可以无限制地发送请求。
- IP 伪造 – 通过使用合法用户的 IP 来伪装请求。
- DDoS 放大 – 将攻击分散到多个伪造的 IP 上。
修复方案
依赖 Express 内置的 req.ip(它会遵循 trust proxy 设置),并正确配置代理信任。
// SECURE CODE
// In app.ts – configure trust proxy
if (env.nodeEnv === 'production') {
app.set('trust proxy', true); // Trust reverse proxy
} else if (process.env.TRUST_PROXY) {
app.set('trust proxy', process.env.TRUST_PROXY);
}
// In rate limiter
keyGenerator: (req) => {
// req.ip respects 'trust proxy' setting
// Express validates X‑Forwarded‑For when trust proxy is configured
return req.ip || 'unknown';
}
关键改进
- 只有在设置了
trust proxy时,Express 才会验证代理头部。 - 不再手动解析头部(容易被伪造)。
- 为生产环境提供了正确的配置。
漏洞 #3:OTP 验证中的竞争条件
问题
在验证一次性密码(OTP)时,检查 OTP 是否有效与标记其已使用之间存在时间窗口。并发请求可以利用这个 TOCTOU(检查时间‑使用时间)竞争条件。
// VULNERABLE CODE (TOCTOU)
const otp = await findOTP(userId, code);
if (!otp || otp.used || otp.expiresAt Math.min(times * 50, 3000),
});
redisClient.on('error', (error) => {
logger.warn('Redis connection error, falling back to memory store');
// express-rate-limit automatically falls back to memory store
});
影响:前后对比
| 之前 | 之后 | |
|---|---|---|
| 时序攻击 | 可枚举用户账户 | 常量时间密码验证(无泄漏) |
| 速率限制 | 可通过 IP 伪造绕过 | 使用 Express trust proxy 正确处理 IP |
| OTP 重用 | 可能因竞争条件导致重复使用 | 原子化 OTP 验证(无竞争条件) |
| 可扩展性 | 速率限制仅限单服务器实例 | 使用 Redis 的分布式速率限制 |
认证安全最佳实践
- 常量时间操作 – 确保安全关键检查无论输入如何都耗时相同;必要时使用虚拟操作。
- 不要直接信任客户端头部 – 通过框架的内置机制(如 Express 的
trust proxy)验证代理头部。 - 使用原子数据库操作 – 对于状态改变的检查(如 OTP 验证),在单个事务中完成检查和更新。
- 部署分布式速率限制 – 使用共享存储(如 Redis)在多实例之间统一限额。
- 保持库的最新 – 定期更新哈希、JWT 以及其他安全相关的依赖。
- 监控并记录认证事件 – 检测异常行为,如快速登录尝试或重复的 OTP 失败。
实施这些实践将显著降低认证系统的攻击面,帮助保护用户凭证。