Security Misconfiguration in Feathersjs with Dynamodb
Security Misconfiguration in Feathersjs with Dynamodb — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time JavaScript applications that often exposes CRUD-style endpoints via services. When these services are backed by AWS DynamoDB, misconfigurations commonly arise from improper authorization and data mapping between FeathersJS hooks and DynamoDB expressions. A typical vulnerability occurs when a FeathersJS service does not enforce field-level or record-level checks before constructing a DynamoDB query. For example, if a service uses a generic find or get without validating that the requesting user is authorized to access the specific item, this can lead to a Broken Level Authorization (BOLA) / Insecure Direct Object Reference (IDOR) pattern.
Consider a FeathersJS service defined with the DynamoDB adapter where the id from the request is directly used as a key in get or as a filter in find. If the authorization logic is implemented only at the FeathersJS layer and not enforced within the shape of the DynamoDB key condition or filter expression, an attacker can manipulate the id or query parameters to access other users' data. For instance, changing userId in the request to another user's identifier may still return data if the service does not scope the DynamoDB query with the requester's identity.
Another specific risk with this combination involves over-permissive DynamoDB condition expressions or missing attribute checks in the item retrieval logic. For example, a service might fetch an item using a key that includes a composite primary key (partition and sort), but omit verification that the item belongs to the tenant or user making the request. This can allow horizontal privilege escalation across tenant boundaries. Additionally, if the FeathersJS service dynamically builds update expressions using user-supplied data without strict validation, it may inadvertently expose attributes that should remain immutable, such as administrative flags or version counters, leading to privilege escalation or data integrity issues.
Real-world attack patterns mirror the OWASP API Top 10 #1 Broken Object Level Authorization, often involving crafted requests to endpoints like /users/:id where the :id is controlled by the client. In DynamoDB, if the key schema uses a composite key such as PK and SK, and the service constructs the key from user input without ensuring it aligns with the authenticated subject, an attacker can enumerate or modify arbitrary records. These misconfigurations are detectable through black-box scanning with middleware that inspects authorization context against DynamoDB key construction, a capability provided by security analysis platforms that test the unauthenticated attack surface in seconds.
Dynamodb-Specific Remediation in Feathersjs — concrete code fixes
To remediate security misconfigurations when using FeathersJS with DynamoDB, you must enforce authorization at the point where DynamoDB queries are constructed and ensure that every key condition and filter includes the authenticated subject. Below are concrete, working examples that demonstrate secure patterns.
1. Scoped retrieval with partition key derived from authentication
Ensure that the partition key (e.g., PK) always includes the user or tenant identifier and that this identifier is taken from the authenticated context, not from user input.
// src/services/users/users.service.js
const { AuthenticationError } = require('@feathersjs/errors');
class SecureUserService {
constructor(amazonDynamoDbClient) {
this.dynamo = amazonDynamoDbClient;
}
async get(id, params) {
const { user } = params;
if (!user || !user.id) {
throw new AuthenticationError('Authentication required');
}
// PK format: USER#
const partitionKey = `USER#${user.id}`;
const response = await this.dynamo.get({
TableName: process.env.USERS_TABLE,
Key: {
PK: { S: partitionKey },
SK: { S: id } // id should be validated to match expected format
}
});
return response.Item;
}
}
module.exports = function () {
const app = this;
const client = new (require('aws-sdk')).DynamoDB.DocumentClient();
app.use('/users', new SecureUserService(client));
};
2. Authorized find with filter expression including ownership
When listing items, append a filter that ties records to the authenticated subject and avoid relying on client-supplied query parameters for scoping.
// src/services/items/items.service.js
class SecureItemService {
constructor(amazonDynamoDbClient) {
this.dynamo = amazonDynamoDbClient;
}
async find(params) {
const { user } = params;
if (!user || !user.sub) {
throw new AuthenticationError('Authentication required');
}
const response = await this.dynamo.query({
TableName: process.env.ITEMS_TABLE,
IndexName: 'OwnerIndex',
KeyConditionExpression: 'ownerId = :ownerId',
FilterExpression: 'orgId = :orgId',
ExpressionAttributeValues: {
':ownerId': { S: `USER#${user.sub}` },
':orgId': { S: user.orgId }
}
}).promise();
return response.Items;
}
}
module.exports = function () {
const app = this;
const client = new (require('aws-sdk')).DynamoDB.DocumentClient();
app.use('/items', new SecureItemService(client));
};
3. Safe update with explicit attribute whitelisting
When patching an item, do not directly pass user payload to DynamoDB update expressions. Instead, validate and whitelist allowed fields, and ensure that the item being updated belongs to the authenticated subject.
// src/services/tasks/tasks.service.js
class SecureTaskService {
constructor(amazonDynamoDbClient) {
this.dynamo = amazonDynamoDbClient;
}
async patch(id, data, params) {
const { user } = params;
if (!user || !user.id) {
throw new AuthenticationError('Authentication required');
}
// Ensure the task belongs to the user
const key = { PK: { S: `TASK#${id}` }, SK: { S: `USER#${user.id}` } };
const updateExpressionParts = [];
const expressionAttributeValues = {};
if (typeof data.status === 'string') {
updateExpressionParts.push('status = :status');
expressionAttributeValues[':status'] = { S: data.status };
}
if (typeof data.dueDate === 'string') {
updateExpressionParts.push('dueDate = :dueDate');
expressionAttributeValues[':dueDate'] = { S: data.dueDate };
}
if (updateExpressionParts.length === 0) {
throw new Error('No valid fields to update');
}
const response = await this.dynamo.update({
TableName: process.env.TASKS_TABLE,
Key: key,
UpdateExpression: `SET ${updateExpressionParts.join(', ')}`,
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: 'UPDATED_NEW'
}).promise();
return response.Attributes;
}
}
module.exports = function () {
const app = this;
const client = new (require('aws-sdk')).DynamoDB.DocumentClient();
app.use('/tasks', new SecureTaskService(client));
};
These examples emphasize that the authenticated subject must influence both the key schema used for retrieval and the filter expressions used for listing. In addition, you should validate input against a strict schema and avoid dynamic construction of update expressions with unchecked user input. Using these patterns reduces the risk of BOLA/IDOR and over-privileged updates when FeathersJS interfaces with DynamoDB.