HIGH timing attackexpressbasic auth

Timing Attack in Express with Basic Auth

Timing Attack in Express with Basic Auth — how this specific combination creates or exposes the vulnerability

A timing attack in Express when Basic Auth is used occurs because the server-side comparison of the supplied credentials does not execute in constant time. Basic Auth sends credentials in an RFC 7617–encoded string that is decoded on the server to obtain a username and a password. If the comparison of the supplied password (or the decoded hash) uses a naive equality check, an attacker can measure response times and infer information about the expected value one byte at a time.

Express does not enforce any built-in mechanism for credential comparison; it is the developer’s responsibility to implement a constant-time comparison. Without it, the route that validates credentials becomes data-dependent in its execution time. For example, when comparing a user-supplied password against a stored hash, a typical JavaScript equality operator (===) short-circuits: it stops evaluating as soon as a mismatch is found. This means that a correct first byte leads to a slightly longer processing time than an incorrect first byte. By sending many requests with carefully crafted credentials and measuring round-trip times, an attacker can gradually deduce the correct value.

The combination of Express and Basic Auth is particularly sensitive because the authentication logic is often implemented inline or in lightweight middleware rather than being delegated to a hardened, specialized component. Consider a naive implementation where the server decodes Basic Auth and compares passwords directly:

const express = require('express');
const app = express();
const users = new Map([['admin', 's3cr3tP@ss']]);

app.get('/protected', (req, res) => {
  const authHeader = req.headers['authorization'];
  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.set('WWW-Authenticate', 'Basic realm="Access"');
    return res.status(401).send('Auth required');
  }
  const base64 = authHeader.split(' ')[1];
  const decoded = Buffer.from(base64, 'base64').toString('utf-8');
  const [username, password] = decoded.split(':');
  const stored = users.get(username);
  if (stored && stored === password) { // timing-vulnerable comparison
    res.send('OK');
  } else {
    res.status(401).send('Invalid credentials');
  }
});

In this example, stored === password is vulnerable. An attacker can send requests with a username of admin and passwords that begin with s; if the response is marginally slower, the first byte is likely correct. Repeating this process byte by byte reveals the password. Even if the stored value is a hash rather than plaintext, comparing the hash byte by byte still leaks information unless a constant-time routine is used.

Timing attacks exploit data-dependent branching and memory access patterns. In the context of Express with Basic Auth, the risk is elevated when custom authentication code is used instead of leveraging established libraries that implement constant-time comparison. The attack is also practical on networks where the attacker can perform repeated measurements with low jitter, such as within the same data center or via optimized network paths. Because the vulnerability is rooted in how strings or buffers are compared, the fix is to ensure all credential checks execute in constant time regardless of input.

Basic Auth-Specific Remediation in Express — concrete code fixes

To mitigate timing attacks in Express with Basic Auth, replace naive equality checks with a constant-time comparison routine. Node.js provides crypto.timingSafeEqual for comparing Buffers of equal length. This function executes in constant time on most platforms, preventing attackers from learning byte-level information through timing differences.

First, ensure both the stored and supplied values are Buffers of the same length. If lengths differ, you should still return a generic failure but use a fixed-duration comparison by comparing against a dummy value of the same length to avoid leaking length information through timing.

Here is a secure implementation that decodes Basic Auth and uses crypto.timingSafeEqual:

const express = require('express');
const crypto = require('crypto');
const app = express();
const users = new Map([['admin', 's3cr3tP@ss']]);

function safeCompare(a, b) {
  if (a.length !== b.length) {
    // Use a dummy comparison to consume a fixed amount of time
    crypto.timingSafeEqual(Buffer.alloc(a.length), a);
    return false;
  }
  return crypto.timingSafeEqual(a, b);
}

app.get('/protected', (req, res) => {
  const authHeader = req.headers['authorization'];
  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.set('WWW-Authenticate', 'Basic realm="Access"');
    return res.status(401).send('Auth required');
  }
  const base64 = authHeader.split(' ')[1];
  const decoded = Buffer.from(base64, 'base64').toString('utf-8');
  const [username, password] = decoded.split(':');
  const stored = users.get(username);
  if (stored && safeCompare(Buffer.from(stored, 'utf-8'), Buffer.from(password, 'utf-8'))) {
    res.send('OK');
  } else {
    res.status(401).send('Invalid credentials');
  }
});

In this example, safeCompare ensures that comparison time does not depend on how many characters match. The length check is performed first, but we still execute a dummy timingSafeEqual when lengths differ to avoid branching based on length in a way that could be measurable. For production use, consider storing a hash (e.g., using scrypt or bcrypt) rather than plaintext passwords, and compare the hash of the supplied password against the stored hash using the same constant-time technique.

Additionally, leverage high-level libraries when possible. For instance, the basic-auth package can parse the header, and combining it with a constant-time comparison utility keeps the implementation concise and secure. The key remediation principle is to ensure that the time taken to validate credentials does not vary with the correctness of the supplied values, thereby neutralizing timing-based inference attacks.

Frequently Asked Questions

Why is timing-safe comparison necessary for Basic Auth in Express?
Timing-safe comparison is necessary because standard equality checks (===) short-circuit on the first differing byte, causing execution time to vary based on how many initial bytes match. In Express with Basic Auth, if the comparison of passwords or hashes is not constant-time, an attacker can infer the correct credential byte by byte by measuring response times, leading to full credential compromise.
Can using HTTPS prevent timing attacks on Basic Auth?
HTTPS protects the confidentiality and integrity of credentials in transit, preventing network eavesdropping. However, it does not prevent timing attacks on the server-side comparison logic. Even over HTTPS, a server-side timing vulnerability in how credentials are compared can be exploited by an attacker who can make authenticated requests and measure response times.