HIGH credential stuffinghapidynamodb

Credential Stuffing in Hapi with Dynamodb

Credential Stuffing in Hapi with Dynamodb — how this specific combination creates or exposes the vulnerability

Credential stuffing is an automated attack where previously breached username and password pairs are used to gain unauthorized access to user accounts. When an API built with Hapi uses Amazon DynamoDB as its user store without adequate protections, the combination can amplify risk if authentication endpoints do not enforce rate limits or anomaly detection. DynamoDB stores user credentials (typically salted hashes), and if an attacker can submit many authentication requests without throttling, they can test large credential lists against valid user accounts.

Hapi is a rich framework that supports multiple authentication strategies, including cookie-based and bearer token schemes. If session management is not bound to a per-user rate limit or if failed authentication responses leak whether a username exists, attackers can iterate through usernames efficiently. DynamoDB queries, such as a GetItem or Query by username, may return different timing or error patterns depending on whether the user exists, enabling account enumeration alongside credential stuffing. The lack of per-IP or per-account attempt limits in the Hapi auth route allows attackers to run scripts that rapidly submit credentials, increasing the likelihood of successful compromise for users who reuse passwords across sites.

OpenAPI specifications exposed by middleBrick can reveal whether authentication routes are missing security schemes or rate limiting. For example, an unauthenticated POST /login endpoint that does not enforce request caps becomes a prime target. DynamoDB provisioned capacity or on-demand settings do not inherently prevent abuse; controls must be implemented in application logic. Without multi-factor authentication or adaptive challenges, credential stuffing can lead to account takeover, data exposure, and lateral movement within the system. MiddleBrick’s 12 security checks, including Authentication, Rate Limiting, and Data Exposure, are designed to identify such gaps by correlating spec definitions with runtime behavior.

Dynamodb-Specific Remediation in Hapi — concrete code fixes

To mitigate credential stuffing in a Hapi application using DynamoDB, implement layered defenses: strict rate limiting on authentication endpoints, consistent error responses, and secure credential handling. Below are concrete code examples that demonstrate these controls.

Rate limiting on login route

Use a rate-limiting strategy that tracks attempts by IP or by normalized user identifier. The following example uses the @hapi/cookie and @hapi/boom packages alongside a simple in-memory map for illustration (in production, use a distributed store like Redis or DynamoDB itself for shared rate-limit state across instances).

// server.js
const Hapi = require('@hapi/hapi');
const bcrypt = require('bcrypt');
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');

const rateLimitWindowMs = 15 * 60 * 1000; // 15 minutes
const maxAttempts = 5;
const attempts = new Map();

const server = Hapi.server({ port: 4000 });

server.route({
  method: 'POST',
  path: '/login',
  config: {
    auth: false,
    handler: async (request, h) => {
      const { username, password } = request.payload;
      const key = `${request.info.remoteAddress}:${username}`;
      const now = Date.now();

      // Clean old attempts and check limit
      if (!attempts.has(key)) {
        attempts.set(key, []);
      }
      const userAttempts = attempts.get(key).filter(t => now - t < rateLimitWindowMs);
      if (userAttempts.length >= maxAttempts) {
        return h.response({ error: 'Too many attempts, try again later' }).code(429);
      }

      const client = new DynamoDBClient({ region: 'us-east-1' });
      const cmd = new GetItemCommand({
        TableName: process.env.USER_TABLE,
        Key: { username: { S: username } }
      });

      try {
        const data = await client.send(cmd);
        if (!data.Item) {
          // Always take the same time to avoid timing leaks
          await bcrypt.hash(password, 10);
          userAttempts.push({ ts: now });
          attempts.set(key, userAttempts);
          return h.response({ error: 'Invalid credentials' }).code(401);
        }
        const match = await bcrypt.compare(password, data.Item.passwordHash.S);
        if (!match) {
          userAttempts.push({ ts: now });
          attempts.set(key, userAttempts);
          return h.response({ error: 'Invalid credentials' }).code(401);
        }
        // Successful authentication: reset attempts and issue token/session
        attempts.delete(key);
        return { token: 'example-jwt-token' };
      } catch (err) {
        return h.response({ error: 'Internal server error' }).code(500);
      }
    }
  }
});

await server.start();

DynamoDB conditional writes for account lockout

Use DynamoDB conditional expressions to implement account lockout counters safely, avoiding race conditions. This example adds a lockedUntil attribute when attempts exceed a threshold.

// lockout.js
const { DynamoDBClient, UpdateItemCommand, GetItemCommand } = require('@aws-sdk/client-dynamodb');

const client = new DynamoDBClient({ region: 'us-east-1' });

async function recordFailedAttempt(username) {
  const now = Date.now();
  const lockoutDurationMs = 30 * 60 * 1000; // 30 minutes

  const getCmd = new GetItemCommand({
    TableName: process.env.USER_TABLE,
    Key: { username: { S: username } },
    ProjectionExpression: 'lockoutCount, lockedUntil'
  });
  const current = await client.send(getCmd);
  const item = current.Item;

  const newCount = (item?.lockoutCount?.N ? parseInt(item.lockoutCount.N, 10) : 0) + 1;
  const shouldLockout = newCount >= 10;
  let lockedUntil = item?.lockedUntil?.S ? new Date(item.lockedUntil.S).getTime() : 0;

  if (shouldLockout && lockedUntil === 0) {
    lockedUntil = now + lockoutDurationMs;
  }

  const updateCmd = new UpdateItemCommand({
    TableName: process.env.USER_TABLE,
    Key: { username: { S: username } },
    UpdateExpression: 'SET lockoutCount = :cnt, lockedUntil = :until',
    ConditionExpression: shouldLockout ? 'attribute_not_exists(lockedUntil) OR lockedUntil < :now' : undefined,
    ExpressionAttributeValues: {
      ':cnt': { N: newCount.toString() },
      ':until': { S: new Date(lockedUntil).toISOString() },
      ':now': { N: now.toString() }
    },
    ReturnValues: 'UPDATED_NEW'
  });

  try {
    await client.send(updateCmd);
    return shouldLockout ? 'locked' : 'attempt_recorded';
  } catch (err) {
    if (err.name === 'ConditionalCheckFailedException') {
      return 'locked'; // lockout already active
    }
    throw err;
  }
}

Consistent error handling and security headers

Ensure that responses do not reveal whether a username exists. Use the same HTTP status code and generic message for authentication failures. Additionally, enforce HTTPS and set security headers to reduce exposure.

// security middleware example
server.ext('onPreResponse', (request, h) => {
  const response = request.response;
  if (response.isBoom) {
    response.output.headers['Strict-Transport-Security'] = 'max-age=63072000';
    response.output.headers['X-Content-Type-Options'] = 'nosniff';
  }
  return h.continue;
});

By combining rate limiting, consistent timing, and DynamoDB-based lockout logic, you reduce the effectiveness of credential stuffing attacks. MiddleBrick scans can validate that these controls are present by checking authentication routes, rate-limiting configurations, and data exposure findings, helping you maintain a robust security posture.

Frequently Asked Questions

How does middleBrick detect missing rate limiting on authentication endpoints?
middleBrick runs parallel security checks, including Rate Limiting and Authentication, comparing your OpenAPI/Swagger spec against runtime behavior. It flags endpoints that accept credentials without request caps and provides remediation guidance.
Can DynamoDB account lockout logic introduce availability issues?
Yes. Conditional writes and frequent updates to lockout attributes can increase consumed read/write capacity. Use on-demand capacity or provisioned capacity with auto-scaling, and keep lockout windows short to balance security and availability.