HIGH nosql injectionexpressdynamodb

Nosql Injection in Express with Dynamodb

Nosql Injection in Express with Dynamodb — how this specific combination creates or exposes the vulnerability

NoSQL injection in an Express service that uses Amazon DynamoDB typically occurs when application code builds query parameters directly from untrusted client input. Unlike SQL, DynamoDB’s condition expressions and key-condition expressions do not use parameterized queries in the same way, and concatenating user-controlled values into these strings can shift the query logic in unintended ways.

Consider an Express endpoint that retrieves a user profile by user ID:

app.get('/profile/:userId', async (req, res) => {
  const userId = req.params.userId;
  const params = {
    TableName: 'users',
    KeyConditionExpression: 'userId = :uid',
    ExpressionAttributeValues: { ':uid': userId }
  };
  const result = await dynamo.get(params).promise();
  res.json(result);
});

At first glance this looks safe because ExpressionAttributeValues is used. However, a vulnerability arises when developers attempt more dynamic queries—such as filtering or sorting—by directly interpolating keys or attribute names. For example, building a filter expression by concatenating a client-supplied field and value is unsafe:

app.get('/search', async (req, res) => {
  const { filterField, filterValue } = req.query;
  const params = {
    TableName: 'users',
    FilterExpression: `${filterField} = :val`,
    ExpressionAttributeValues: { ':val': filterValue }
  };
  const result = await dynamo.scan(params).promise();
  res.json(result);
});

Although ExpressionAttributeValues protects values, attribute names cannot be bound as parameters. An attacker providing filterField as userId OR attribute_exists(/* malicious condition */) can alter the logic of the filter. In a scan or query this can unintentionally expose more items than intended, and in worst-case scenarios it can interfere with access patterns relied upon by authorization checks (BOLA/IDOR).

DynamoDB’s conditional writes and update expressions suffer the same class of issue when attribute names or condition subexpressions are composed from untrusted input. For instance:

app.patch('/update-score', async (req, res) => {
  const { userId, operator, operand } = req.body;
  // UNSAFE: operator and operand used directly in expression string
  const params = {
    TableName: 'users',
    Key: { userId },
    UpdateExpression: `SET score = score ${operator} :operand`,
    ExpressionAttributeValues: { ':operand': operand },
    ConditionExpression: 'score >= :min'
  };
  await dynamo.update(params).promise();
  res.sendStatus(200);
});

If operator is user-controlled, an attacker can supply values like >> 0 or chain expressions to escalate privileges or bypass intended constraints. Because DynamoDB parses these strings as part of its expression language, injection here can modify query semantics, enable unauthorized data access, or lead to data corruption.

Additionally, unauthenticated endpoints that expose DynamoDB interfaces—such as GraphQL resolvers or Lambda functions invoked without robust authorization—amplify risk. The middleBrick LLM/AI Security checks specifically test for unauthenticated LLM endpoints and output leakage; similar care is required for any public API surface that interacts with DynamoDB to ensure attackers cannot probe or manipulate queries through indirect paths.

Dynamodb-Specific Remediation in Express — concrete code fixes

Remediation centers on strict validation, allowlisting, and avoiding string composition for expression components. Attribute names must never be concatenated directly; use allowlists to map incoming keys to safe attribute names.

Safe query-by-userId with Express path parameter:

app.get('/profile/:userId', async (req, res) => {
  const userId = req.params.userId;
  // Validate userId format before using it
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(userId)) {
    return res.status(400).json({ error: 'invalid userId' });
  }
  const params = {
    TableName: 'users',
    KeyConditionExpression: 'userId = :uid',
    ExpressionAttributeValues: { ':uid': userId }
  };
  const result = await dynamo.get(params).promise();
  res.json(result);
});

For dynamic filtering, use an allowlist instead of direct concatenation:

const ALLOWED_FILTERS = new Set(['email', 'status', 'role']);
app.get('/search', async (req, res) => {
  const filterField = req.query.filterField;
  const filterValue = req.query.filterValue;
  if (!ALLOWED_FILTERS.has(filterField)) {
  return res.status(400).json({ error: 'unsupported filter' });
  }
  const params = {
    TableName: 'users',
    FilterExpression: '#field = :val',
    ExpressionAttributeNames: { '#field': filterField },
    ExpressionAttributeValues: { ':val': filterValue }
  };
  const result = await dynamo.scan(params).promise();
  res.json(result);
});

For updates, validate operator and operand rather than embedding them directly:

app.patch('/update-score', async (req, res) => {
  const { userId, operator, operand } = req.body;
  const allowedOperators = new Set(['+', '-']);
  if (!allowedOperators.has(operator)) {
    return res.status(400).json({ error: 'invalid operator' });
  }
  // Validate operand is a number to avoid injection via expression language
  if (typeof operand !== 'number' || !Number.isFinite(operand)) {
    return res.status(400).json({ error: 'invalid operand' });
  }
  const params = {
    TableName: 'users',
    Key: { userId },
    UpdateExpression: 'SET score = score :op :val',
    ExpressionAttributeValues: { ':op': operator, ':val': operand },
    ConditionExpression: 'score >= :min'
  };
  await dynamo.update(params).promise();
  res.sendStatus(200);
});

Additional best practices:

  • Use ExpressionAttributeNames for any user-influenced field names, even when they come from an allowlist, to reserve reserved words and reduce injection surface.
  • Validate input types and lengths rigorously; DynamoDB’s query and scan behaviors can vary with malformed or overly large inputs.
  • Enforce authorization checks server-side on every request rather than relying on client-supplied filters or query hints.
  • Leverage middleware to normalize and sanitize inputs before they reach DynamoDB calls.

By treating attribute names and operators as untrusted and enforcing strict allowlists, Express services can safely interact with DynamoDB while minimizing NoSQL injection risk.

Frequently Asked Questions

Can using ExpressionAttributeNames fully prevent NoSQL injection in DynamoDB?
ExpressionAttributeNames help prevent injection for attribute names, but they must be paired with strict allowlists and server-side validation. They do not protect query or filter logic; you still need to validate and constrain all user-controlled input.
How does middleBrick handle DynamoDB-related findings?
middleBrick scans the unauthenticated attack surface and maps findings to frameworks such as OWASP API Top 10. For Express services using DynamoDB, it highlights injection risks in query construction and provides remediation guidance without fixing or blocking.