Vulnerable Components in Feathersjs with Dynamodb
Vulnerable Components in Feathersjs with Dynamodb — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time APIs that typically exposes REST and WebSocket endpoints through services. When using Amazon DynamoDB as the persistence layer, several classes of vulnerabilities can emerge from the interaction between FeathersJS service logic and how DynamoDB requests are constructed and authorized.
One common pattern is defining a Feathers service with a custom find or get hook that builds a DynamoDB query from incoming query parameters without strict allowlisting. If the service directly passes user-controlled filters to DynamoDB Query or Scan operations, it can enable BOLA (Broken Object Level Authorization) and IDOR when the requester can manipulate key condition expressions or filter arbitrary attributes. For example, a service that uses params.query.id to form a KeyConditionExpression without verifying ownership can allow one user to read another user’s record by changing the ID in the request.
Input validation issues are also prominent. DynamoDB has specific data shape and size constraints; if FeathersJS does not validate incoming payloads before building PutItem or UpdateItem requests, attackers can submit oversized attribute names, unexpected type structures, or reserved keyword keys that cause partial writes or injection-like behavior. A lack of type checks may lead to ConditionalCheckFailedException being mishandled, which can mask authorization logic flaws or lead to inconsistent state.
Property authorization gaps occur when read or write permissions are encoded in application logic rather than in the request context. For instance, a service might fetch an item using a DynamoDB GetItem, then decide in FeathersJS whether to return it based on user roles. If this check is performed after the read, sensitive data has already left the persistence layer, increasing data exposure risk. Similarly, privilege escalation can arise if an endpoint accepts an isAdmin or role field in the request body and uses it directly in a DynamoDB update to elevate permissions.
Unsafe consumption patterns include using unvalidated results from DynamoDB for redirects or embedding raw database metadata in responses, which can lead to SSRF or information leakage. If a service stores URLs or file paths in DynamoDB and later uses them without validation, an attacker may be able to reference internal resources. Unauthenticated LLM endpoints are not typical in this stack, but if an API exposes model-inference endpoints that accept DynamoDB-sourced prompts without guarding against prompt injection, LLM security risks can appear.
Finally, rate limiting and inventory management misconfigurations can amplify the impact of insecure defaults. If FeathersJS does not enforce per-user rate limits at the service layer, a malicious actor can exhaust write capacity or trigger excessive DynamoDB consumed capacity. Inventory management flaws may appear when services do not reconcile state changes in DynamoDB transactions, leading to logic bugs such as double-spending or race conditions in reservation systems.
Dynamodb-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on strict input validation, explicit allowlisting of query parameters, and ensuring authorization checks happen before any DynamoDB operation. Below are concrete, realistic examples for FeathersJS services using the AWS SDK for JavaScript v3.
1. Safe Find with Key Condition Expression
Do not build KeyConditionExpression from raw user input. Instead, extract a known user identifier and enforce ownership before querying.
// services/users/users.service.js
const { DynamoDBClient, QueryCommand } = require('@aws-sdk/client-dynamodb');
class UsersService {
constructor() {
this.dynamo = new DynamoDBClient({});
this.table = process.env.DYNAMODB_TABLE || 'users';
}
async find(params) {
const userId = params.user.id; // authenticated user id from auth hook
const command = new QueryCommand({
TableName: this.table,
KeyConditionExpression: 'userId = :uid',
ExpressionAttributeValues: {
':uid': { S: userId }
}
});
const result = await this.dynamo.send(command);
return result.Items || [];
}
}
module.exports = function () {
const app = this;
app.use('/users', new UsersService());
};
2. Input Validation and Field Allowlisting
Validate and sanitize all incoming data before using it in DynamoDB expressions. Use a library or custom checks to ensure types and lengths are safe.
// validation.js
function validateUpdatePayload(payload) {
const allowedFields = ['displayName', 'email', 'preferences'];
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid payload');
}
for (const key of Object.keys(payload)) {
if (!allowedFields.includes(key)) {
throw new Error(`Field not allowed: ${key}`);
}
}
if (payload.displayName && typeof payload.displayName !== 'string') {
throw new Error('displayName must be a string');
}
return true;
}
// services/display-names.service.js
async update(id, data, params) {
validateUpdatePayload(data);
const userId = params.user.id;
const command = new UpdateCommand({
TableName: this.table,
Key: { userId: { S: userId }, id: { S: id } },
UpdateExpression: 'set #dn = :val',
ExpressionAttributeNames: { '#dn': 'displayName' },
ExpressionAttributeValues: {
':val': { S: data.displayName }
},
ConditionExpression: 'attribute_exists(userId)'
});
await this.dynamo.send(command);
return data;
}
3. Authorization Before Read (Avoiding Data Exposure)
Fetch the record first, then enforce that the requester owns it. This ensures sensitive data is not returned to unauthorized users even if the key condition is manipulated.
async get(id, params) {
const userId = params.user.id;
const command = new GetCommand({
TableName: this.table,
Key: { userId: { S: userId }, id: { S: id } }
});
const item = await this.dynamo.send(command);
if (!item) {
throw new Error('Not found');
}
return item;
}
4. Preventing Privilege Escalation
Never allow client-supplied fields like role or isAdmin to directly update privileged attributes. Explicitly exclude or ignore them in the update expression.
async update(id, data, params) {
const userId = params.user.id;
const updateFields = [];
const attrValues = {};
if (typeof data.displayName === 'string') {
updateFields.push('#dn = :dn');
attrValues[':dn'] = { S: data.displayName };
}
if (typeof data.status === 'string') {
updateFields.push('#st = :st');
attrValues[':st'] = { S: data.status };
}
if (updateFields.length === 0) {
throw new Error('No valid fields to update');
}
const command = new UpdateCommand({
TableName: this.table,
Key: { userId: { S: userId }, id: { S: id } },
UpdateExpression: `set ${updateFields.join(', ')}`,
ExpressionAttributeNames: { '#dn': 'displayName' },
ExpressionAttributeValues: { ...attrValues },
ConditionExpression: 'attribute_exists(userId)'
});
await this.dynamo.send(command);
return { id, ...data };
}
5. Safe Error Handling to Avoid Information Leakage
Do not expose DynamoDB error details in responses. Map conditional check failures to generic not-found or unauthorized responses to avoid leaking schema or state information.
async remove(id, params) {
const userId = params.user.id;
try {
const command = new DeleteCommand({
TableName: this.table,
Key: { userId: { S: userId }, id: { S: id } },
ConditionExpression: 'attribute_exists(userId)'
});
await this.dynamo.send(command);
} catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
throw new Error('Not found or insufficient permissions');
}
throw err;
}
}
6. Rate Limiting and Inventory Safeguards
Implement application-level checks or leverage API gateway capabilities to limit per-user request rates. For critical inventory operations, use DynamoDB transactions or conditional updates to ensure consistency.
async reserveItem(userId, itemId, quantity, params) {
const command = new UpdateCommand({
TableName: this.table,
Key: { userId: { S: userId }, itemId: { S: itemId } },
UpdateExpression: 'SET #q = if_not_exists(#q, :zero) - :dec',
ExpressionAttributeNames: { '#q': 'quantity' },
ExpressionAttributeValues: {
':dec': { N: String(quantity) },
':zero': { N: '0' }
},
ConditionExpression: 'attribute_exists(userId) AND #q >= :dec'
});
await this.dynamo.send(command);
return { success: true };
}