HIGH credential stuffingfeathersjsbasic auth

Credential Stuffing in Feathersjs with Basic Auth

Credential Stuffing in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability

Credential stuffing is an automated attack where lists of breached username and password pairs are used to sign in to accounts. When a Feathersjs service is configured with Basic Auth and lacks protections, each request carries an Authorization header of the form Authorization: Basic base64(username:password). Because Basic Auth sends credentials with every request, attackers can replay stolen credentials directly and the server will treat them as valid authenticated requests if no additional checks exist.

Feathersjs does not provide built-in protections against credential stuffing by default. If you add Basic Auth via a custom authentication hook without rate limiting, account lockout, or multi-factor options, each request is evaluated independently. An attacker can rotate through thousands of credential pairs against the same endpoint, and successful logins will return a 200 response and a JWT or session token, confirming valid credentials. This becomes especially risky if usernames are predictable (e.g., emails) and passwords are reused across sites, a common pattern in credential stuffing campaigns.

The unauthenticated attack surface tested by middleBrick exposes these risks without requiring credentials. During a scan, checks for Authentication and Rate Limiting probe the service to determine whether repeated failed attempts are monitored or throttled. A Feathersjs endpoint using only Basic Auth typically receives a high risk finding if there is no evidence of login attempt throttling, account lockout, or anomaly detection, because the specification alone does not enforce these safeguards.

In practice, a vulnerable Feathersjs service might define an authentication hook that verifies the Basic Auth header but does not track failed attempts:

// services/auth/hooks.js
const { AuthenticationError } = require('@feathersjs/errors');
const basicAuth = require('basic-auth');

module.exports = function () {
  return async context => {
    const req = context.params.raw;
    const user = basicAuth(req);
    if (!user) {
      throw new AuthenticationError('Authentication required');
    }
    const { name, pass } = user;
    // Insecure: direct comparison without rate limiting
    const account = await context.app.service('accounts').find({ query: { email: name } });
    const record = account.data[0];
    if (!record || record.password !== pass) {
      throw new AuthenticationError('Invalid credentials');
    }
    context.params.account = record;
    context.params.accessToken = { ... };
    return context;
  };
};

In this example, an attacker can iterate through credential lists rapidly, because the service does not limit attempts per identity or source IP. middleBrick’s checks for Rate Limiting and Authentication highlight the absence of these controls, producing findings with severity High for missing brute-force protections and recommendations to add throttling or lockout mechanisms.

Basic Auth-Specific Remediation in Feathersjs — concrete code fixes

Remediation focuses on reducing the attack surface for credential stuffing when using Basic Auth in Feathersjs. Combine transport-layer protections, request-rate controls, and identity-based throttling. Do not rely on Basic Auth alone; treat it as a transport mechanism and add additional guards.

  • Enforce HTTPS to prevent credential interception. middleBrick’s Encryption and Data Exposure checks verify that credentials are transmitted securely.
  • Add rate limiting at the service or global hook level to restrict attempts per IP or per user identity.
  • Implement progressive delays or account lockout after repeated failures for the same username.
  • Use a secure password storage mechanism (e.g., bcrypt) so that server-side comparison is not plaintext password matching.

Below is a hardened example that adds rate limiting using an in-memory map for simplicity. In production, use a shared store like Redis to coordinate limits across instances.

// services/auth/hooks.js
const { AuthenticationError, GeneralError } = require('@feathersjs/errors');
const basicAuth = require('basic-auth');

const attempts = new Map(); // key: "username:ip", value: { count, lastAttempt }
const MAX_ATTEMPTS = 5;
const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
const BLOCK_MINUTES = 30;

function isBlocked(username, ip) {
  const key = `${username}:${ip}`;
  const entry = attempts.get(key);
  if (!entry) return false;
  const now = Date.now();
  if (now - entry.lastAttempt > WINDOW_MS) {
    attempts.delete(key);
    return false;
  }
  return entry.count >= MAX_ATTEMPTS;
}

function recordAttempt(username, ip) {
  const key = `${username}:${ip}`;
  const entry = attempts.get(key);
  if (!entry) {
    attempts.set(key, { count: 1, lastAttempt: Date.now() });
    return;
  }
  entry.count += 1;
  entry.lastAttempt = Date.now();
  if (entry.count >= MAX_ATTEMPTS) {
    setTimeout(() => attempts.delete(key), BLOCK_MINUTES * 60 * 1000);
  }
}

module.exports = function () {
  return async context => {
    const req = context.params.raw;
    const user = basicAuth(req);
    if (!user) {
      throw new AuthenticationError('Authentication required');
    }
    const { name, pass } = user;
    const ip = (req.connection && req.connection.remoteAddress) ||
               (req.socket && req.socket.remoteAddress) ||
               req.headers['x-forwarded-for'] || 'unknown';
    if (isBlocked(name, ip)) {
      throw new GeneralError('Too many attempts, try again later', 429);
    }
    // Replace with a secure user lookup and hashed password check
    const account = await context.app.service('accounts').find({ query: { email: name } });
    const record = account.data[0];
    if (!record || record.password !== pass) { // In production, use bcrypt.compare
      recordAttempt(name, ip);
      throw new AuthenticationError('Invalid credentials');
    }
    // Reset on success
    attempts.delete(`${name}:${ip}`);
    context.params.account = record;
    context.params.accessToken = { ... };
    return context;
  };
};

For production, replace the in-memory map with a persistent store and use a constant-time comparison for passwords (e.g., bcrypt). middleBrick’s Pro plan enables continuous monitoring so that risk scores are recalculated on a configurable schedule, and its GitHub Action can fail builds if a scan drops below your chosen threshold, helping you catch regressions before deployment.

When you integrate the MCP Server, you can scan APIs directly from your AI coding assistant within your development environment, ensuring that new services or changes to authentication hooks are evaluated early. The dashboard lets you track scores over time and review prioritized findings with severity and remediation guidance.

Frequently Asked Questions

Does middleBrick fix credential stuffing vulnerabilities in Feathersjs?
No. middleBrick detects and reports findings with remediation guidance; it does not fix, patch, or block issues. You must implement the recommended controls such as rate limiting and secure password storage.
How often should I scan a Feathersjs API for credential stuffing risks?
Use continuous monitoring in the Pro plan to scan on a configurable schedule. For critical changes, run a scan after each deployment or configuration update to verify that protections like rate limiting remain in place.