The Hidden Vulnerabilities in Your Authentication System: A Deep Dive into Timing Attacks, IP Spoofing, and Race Conditions
Source: Dev.to
Introduction
Authentication is the foundation of application security. Yet, many developers—even experienced ones—overlook subtle vulnerabilities that can expose user data, enable account enumeration, or allow attackers to bypass rate limiting. Over the past few months I’ve been hardening an authentication service and discovered three critical vulnerabilities that are surprisingly common in production systems.
In this article I’ll walk through these vulnerabilities, explain why they’re dangerous, and show how to fix them. Whether you’re building your own auth system or evaluating third‑party solutions, understanding these issues is crucial.
Vulnerability #1: Timing Attacks on Password Verification
The Problem
An attacker can guess a user’s password by sending login requests with different passwords and measuring response time. If the server takes longer to respond when the username exists (even with a wrong password), the attacker learns that the account exists—a classic timing attack.
What was happening
When a user doesn’t exist, the function returns immediately. When a user exists but the password is wrong, it performs the expensive password verification first. This timing difference leaks information.
Why It Matters
- Account enumeration – attackers can determine which email addresses are registered.
- User privacy – reveals account existence.
- Targeted attacks – attackers can focus on known accounts.
The Fix
Implement constant‑time verification by always performing password verification, even for non‑existent users.
// SECURE CODE
const user = await findUserByEmail(email);
const DUMMY_HASH = '$argon2id$v=19$m=65536,t=3,p=4$...'; // Pre‑computed dummy hash
const DUMMY_PASSWORD = 'invalid';
// Always perform both verifications in parallel
const [, realResult] = await Promise.all([
verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // Dummy verification (always fails)
user && user.passwordHash
? verifyPassword(user.passwordHash, input.password)
: verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // Dummy if no user
]);
if (!user || !user.passwordHash || !realResult) {
throw new AuthError('Invalid credentials');
}
Key improvements
- Both paths take similar time (constant‑time execution).
- No information leakage about user existence.
- Parallel execution maintains performance.
Vulnerability #2: IP Spoofing in Rate Limiting
The Problem
Rate limiting is essential for preventing brute‑force attacks, but many implementations trust the X‑Forwarded‑For header directly, which attackers can spoof.
// VULNERABLE CODE
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
// Use IP for rate limiting
Why It Matters
- Bypass rate limits – attackers can make unlimited requests.
- IP spoofing – frame legitimate users by using their IPs.
- DDoS amplification – distribute attacks across multiple fake IPs.
The Fix
Rely on Express’s built‑in req.ip, which respects the trust proxy setting, and configure the proxy trust correctly.
// 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';
}
Key improvements
- Express validates proxy headers only when
trust proxyis set. - No manual header parsing (which can be spoofed).
- Proper configuration for production deployments.
Vulnerability #3: Race Condition in OTP Verification
The Problem
When verifying one‑time passwords (OTPs), there’s a window between checking if an OTP is valid and marking it as used. Concurrent requests can exploit this TOCTOU (time‑of‑check, time‑of‑use) race condition.
// 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
});
Impact: Before vs. After
| Before | After | |
|---|---|---|
| Timing attacks | Could enumerate user accounts | Constant‑time password verification (no leakage) |
| Rate limiting | Bypassed via IP spoofing | Proper IP handling with Express trust proxy |
| OTP reuse | Possible through race conditions | Atomic OTP verification (no race conditions) |
| Scaling | Rate limiting only per‑server instance | Distributed rate limiting with Redis |
Best Practices for Authentication Security
- Constant‑time operations – Ensure security‑critical checks take the same amount of time regardless of input; use dummy operations when needed.
- Never trust client headers directly – Validate proxy headers through your framework’s built‑in mechanisms (e.g., Express’s
trust proxy). - Use atomic database operations – For state‑changing checks (like OTP verification), perform the check and update in a single transaction.
- Deploy distributed rate limiting – Use a shared store such as Redis to enforce limits consistently across multiple instances.
- Keep libraries up to date – Regularly update hashing, JWT, and other security‑related dependencies.
- Monitor and log authentication events – Detect anomalies such as rapid login attempts or repeated OTP failures.
Implementing these practices will dramatically reduce the attack surface of your authentication system and help protect your users’ credentials.