The Hidden Vulnerabilities in Your Authentication System: A Deep Dive into Timing Attacks, IP Spoofing, and Race Conditions

Published: (December 7, 2025 at 03:24 PM EST)
4 min read
Source: Dev.to

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 proxy is 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

BeforeAfter
Timing attacksCould enumerate user accountsConstant‑time password verification (no leakage)
Rate limitingBypassed via IP spoofingProper IP handling with Express trust proxy
OTP reusePossible through race conditionsAtomic OTP verification (no race conditions)
ScalingRate limiting only per‑server instanceDistributed 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.

Back to Blog

Related posts

Read more »