HIGH rate limiting bypassadonisjsdynamodb

Rate Limiting Bypass in Adonisjs with Dynamodb

Rate Limiting Bypass in Adonisjs with Dynamodb — how this specific combination creates or exposes the vulnerability

AdonisJS, a Node.js web framework, typically relies on in-memory or file-based rate limit stores. When configured to use DynamoDB as the backing store, implementation details can introduce a bypass if the rate-limiting key does not uniquely and reliably identify the client. DynamoDB’s eventual consistency and conditional-write behavior can also affect how limits are enforced across distributed instances.

A common pattern is to key records by a combination such as ip + endpoint or user_id + endpoint. If the key is derived from untrusted data that can be trivially altered, an attacker can force each request to use a distinct key, effectively evading the intended limit. For example, rotating source IPs (via proxy chains or NAT), or manipulating a mutable identifier included in the key, can fragment enforcement across many logical buckets.

DynamoDB conditional writes are often used to create entries only when they do not exist. If the conditional check does not enforce a strong uniqueness constraint aligned with the intended policy, concurrent requests with slightly different keys can each pass the condition and create separate capacity entries. This can allow an attacker to issue many requests that map to different logical keys but the same real-world client.

Consider an AdonisJS route using a custom RateLimiter service that builds a DynamoDB key from a header the client can set:

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

class RateLimiter {
  constructor(table) {
    this.client = new DynamoDBClient({});
    this.table = table;
  }

  async allow(key, limit, windowMs) {
    const now = Date.now();
    const minTimestamp = now - windowMs;
    const params = {
      TableName: this.table,
      Key: { pk: { S: `rate#${key}` } },
    };
    const cmd = new GetItemCommand(params);
    const { Item } = await this.client.send(cmd);
    if (!Item) {
      const putParams = {
        TableName: this.table,
        Item: {
          pk: { S: `rate#${key}` },
          timestamp: { N: String(now) },
          count: { N: '1' },
        },
        ConditionExpression: 'attribute_not_exists(pk)',
      };
      try {
        await this.client.send(new PutItemCommand(putParams));
        return true;
      } catch (err) {
        if (err.name === 'ConditionalCheckFailedException') {
          return false;
        }
        throw err;
      }
    }
    const count = parseInt(Item.count.N, 10);
    if (count >= limit) return false;
    const updateParams = {
      TableName: this.table,
      Key: { pk: { S: `rate#${key}` } },
      UpdateExpression: 'SET #c = :inc, #t = :ts',
      ExpressionAttributeNames: { '#c': 'count', '#t': 'timestamp' },
      ExpressionAttributeValues: { ':inc': { N: String(count + 1) }, ':ts': { N: String(now) } },
    };
    await this.client.send(new PutItemCommand(updateParams));
    return true;
  }
}

module.exports = RateLimiter;

If the key is derived from something the client controls (e.g., a user-supplied X-Account-ID header) or is not sufficiently bound to the client’s network identity, an attacker can enumerate keys or rotate identifiers to stay under the limit. Additionally, DynamoDB’s GetItem followed by conditional PutItem is not atomic across all edge cases; race conditions can occur under high concurrency, allowing slightly more requests than intended.

To map this to the middleBrick scan: the tool’s 12 checks include Rate Limiting and Input Validation, and when scanning an AdonisJS endpoint backed by DynamoDB, it will flag findings such as inconsistent keying, missing binding of client identity, or over-privileged conditional writes. These findings appear in the dashboard and CLI output with severity and remediation guidance, helping you understand how implementation choices can weaken intended limits.

Dynamodb-Specific Remediation in Adonisjs — concrete code fixes

Remediation focuses on constructing stable, client-bound keys and ensuring updates are performed atomically. Use a key that reliably identifies the client or tenant and avoid incorporating mutable or attacker-controlled values. Prefer a single-item update with an atomic increment to avoid read-before-write races.

Example: key by normalized IP and endpoint, with an atomic increment using UpdateItem and a TTL for cleanup:

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

class RateLimiter {
  constructor(table) {
    this.client = new DynamoDBClient({});
    this.table = table;
  }

  async allow(clientId, endpoint, limit, windowSec) {
    const key = `rate#${clientId}#${endpoint}`;
    const now = Math.floor(Date.now() / 1000);
    const updateParams = {
      TableName: this.table,
      Key: { pk: { S: key } },
      UpdateExpression: 'SET #c = if_not_exists(#c, :zero) + :inc, #e = :ep, #t = :ts',
      ConditionExpression: '#c < :limit',
      ExpressionAttributeNames: { '#c': 'count', '#e': 'endpoint', '#t': 'timestamp' },
      ExpressionAttributeValues: {
        ':inc': { N: '1' },
        ':limit': { N: String(limit) },
        ':ep': { S: endpoint },
        ':ts': { N: String(now + windowSec) },
        ':zero': { N: '0' },
      },
    };
    try {
      await this.client.send(new UpdateItemCommand(updateParams));
      return true;
    } catch (err) {
      if (err.name === 'ConditionalCheckFailedException') {
        return false;
      }
      throw err;
    }
  }
}

module.exports = RateLimiter;

Key improvements:

  • clientId should be derived from a stable, trusted source (e.g., API key ID or a normalized IP tied to the request), not from user-controlled headers alone.
  • A single UpdateItem with an atomic increment and a conditional check avoids read-modify-write races.
  • An expiration timestamp (#t) can be used with DynamoDB TTL to auto-expire old entries, avoiding unbounded growth.

For multi-region deployments, prefer a strongly consistent read before the conditional write if you need stricter guarantees, but note this may affect performance. Alternatively, design keys so that all requests from the same client map to a single partition key to avoid cross-partition coordination.

middleBrick’s scans will surface misconfigurations such as weak keys, missing conditions, or missing TTLs. With the Pro plan, you can enable continuous monitoring so future regressions in rate-limiting logic are flagged immediately, and the GitHub Action can fail builds if a score drops below your chosen threshold.

Related CWEs: resourceConsumption

CWE IDNameSeverity
CWE-400Uncontrolled Resource Consumption HIGH
CWE-770Allocation of Resources Without Limits MEDIUM
CWE-799Improper Control of Interaction Frequency MEDIUM
CWE-835Infinite Loop HIGH
CWE-1050Excessive Platform Resource Consumption MEDIUM

Frequently Asked Questions

Can DynamoDB eventual consistency cause rate limits to be misapplied in AdonisJS?
Yes. If your implementation relies on a read followed by a conditional write, eventual consistency can allow concurrent requests to pass the check before the write propagates. Use atomic UpdateItem with a condition to avoid this.
What key strategy prevents rate-limiting bypass via mutable client inputs in AdonisJS with DynamoDB?
Derive the DynamoDB key from a trusted, immutable client identifier (e.g., API key ID or a normalized, validated IP) and avoid incorporating attacker-controlled headers directly. Bind the key to the intended endpoint and use atomic increments in a single UpdateItem call.