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
ExpressionAttributeNamesfor 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.