HIGH timing attackfeathersjsapi keys

Timing Attack in Feathersjs with Api Keys

Timing Attack in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability

A timing attack in a Feathersjs service that uses API keys can occur when key validation logic does not execute in constant time. Feathersjs is often used with an authentication layer that checks an incoming API key against a database or configuration. If the comparison between the submitted key and the stored key is performed using a standard equality check (e.g., keyStored === keyProvided), the operation may short-circuit: upon the first mismatched character, the check stops and returns false. This causes variable execution time that depends on how many leading characters match. An attacker who can send multiple requests and measure response times can infer whether a prefix of the API key is correct, gradually learning the full key byte by byte without ever triggering an authentication failure from invalid key format.

In Feathersjs, this typically arises in a custom hook or service that manually validates an API key header (e.g., authorization: ApiKey <key>) before allowing access to a service. If the lookup or comparison logic is not hardened, the unauthenticated attack surface includes endpoints that accept API keys, and the timing differences can be measurable across network hops. For example, a hook that does a database find by key and then compares may leak information through response time differences depending on how early the mismatch occurs. An attacker can use these differences to distinguish a valid key prefix from an invalid one, effectively performing a remote timing oracle against the API. This becomes especially relevant when API keys are used for access control without additional protections such as constant-time comparison or rate limiting that obscures timing signals.

The risk is compounded when the API key is accepted in unauthenticated (black-box) scans, as the scanner can send many requests and observe subtle timing deviations. Because Feathersjs services often expose multiple endpoints, an attacker can correlate timing behavior across endpoints that share the same validation logic. While Feathersjs itself does not introduce the vulnerability, insecure implementation of key validation does. Proper mitigation requires ensuring that key checks execute in constant time and that the service does not inadvertently expose timing information through variable response behavior.

Api Keys-Specific Remediation in Feathersjs — concrete code fixes

To remediate timing risks with API keys in Feathersjs, replace standard equality comparisons with a constant-time comparison routine and ensure that key lookup does not leak validity through timing or error messages. Below are concrete, working examples that you can adapt to your service.

1. Constant-time comparison utility

Use a constant-time comparison function that always iterates over the full length of the expected key, preventing early-exit timing leaks. Node.js provides crypto.timingSafeEqual for this purpose when comparing Buffers of equal length.

// utils/constantTimeCompare.js
const crypto = require('node:crypto');

/**
 * Constant-time comparison for strings or buffers.
 * Returns true if `a` and `b` are equal, false otherwise.
 * Always performs work proportional to the length of `a`.
 */
function timingSafeEqual(a, b) {
  // Normalize inputs to Buffer
  const bufA = Buffer.isBuffer(a) ? a : Buffer.from(a, 'utf8');
  const bufB = Buffer.isBuffer(b) ? b : Buffer.from(b, 'utf8');

  // Lengths must match to avoid leaking size information
  if (bufA.length !== bufB.length) {
    return false;
  }

  return crypto.timingSafeEqual(bufA, bufB);
}

module.exports = { timingSafeEqual };

Example: Hook validating API key using constant-time comparison

In a Feathersjs service, create an authentication hook that retrieves the stored key and compares it using the constant-time utility. Avoid returning early on mismatch and avoid branching on key validity before the comparison.

// hooks/authenticate-api-key.js
const { timingSafeEqual } = require('../utils/constantTimeCompare');

module.exports = function authenticateApiKey(options = {}) {
  return async context => {
    const { app, params } = context;
    const providedKey = context.params.headers && context.params.headers['authorization'];

    if (!providedKey || !providedKey.startsWith('ApiKey ')) {
      // Return a generic error without indicating whether the key was malformed
      throw new Error('Unauthorized');
    }

    const provided = providedKey.slice('ApiKey '.length);

    // Example: fetch stored key from a service (e.g., users/settings)
    // Ensure this lookup does not leak timing via absence/presence of record.
    // One approach: always use a fixed dummy key when no record exists.
    const storedRecord = await app.service('api-keys').find({ query: { $limit: 1 } });
    const storedKey = (storedRecord && storedRecord.data && storedRecord.data[0] && storedRecord.data[0].key) || '';

    // Use constant-time comparison to avoid timing leaks
    const isValid = timingSafeEqual(storedKey, provided);
    if (!isValid) {
      throw new Error('Unauthorized');
    }

    // Attach identity or scopes to context for downstream use
    context.result = { authenticated: true, scope: 'api-key' };
    return context;
  };
};

// Attach the hook to a service
const someService = app.service('some-path');
someService.hooks({
  before: {
    all: [require('./hooks/authenticate-api-key')]
  }
});

Example: Using environment-stored key with constant-time comparison

If you store a single API key in environment variables (e.g., for simplicity), compare the provided key against the environment key using the same constant-time utility. This avoids timing leaks while keeping deployment straightforward.

// hooks/authenticate-api-key-env.js
const { timingSafeEqual } = require('../utils/constantTimeCompare');

module.exports = function authenticateApiKeyEnv() {
  return async context => {
    const providedKey = context.params.headers && context.params.headers['authorization'];
    if (!providedKey || !providedKey.startsWith('ApiKey ')) {
      throw new Error('Unauthorized');
    }

    const provided = providedKey.slice('ApiKey '.length);
    const stored = process.env.API_KEY || '';

    if (!timingSafeEqual(stored, provided)) {
      throw new Error('Unauthorized');
    }

    context.result = { authenticated: true };
    return context;
  };
};

// Usage in service initialization
const app = require('feathers')();
app.configure(require('./hooks/authenticate-api-key-env'));
app.use('/secure', require('./secure-service'));

Frequently Asked Questions

Why does a standard equality check create a timing risk in Feathersjs with API keys?
Standard equality (===) short-circuits on the first mismatching character, so execution time varies based on how many leading characters match. An attacker can send requests with candidate keys and measure response times to progressively learn the correct key.
Does using a database lookup for the API key prevent timing attacks?
A database lookup alone does not prevent timing attacks if the key comparison afterward is not constant-time. Additionally, differences in lookup timing (e.g., index misses vs hits) can also leak information; always combine constant-time comparison with consistent handling of missing records.