Insecure Direct Object Reference in Feathersjs with Dynamodb
Insecure Direct Object Reference in Feathersjs with Dynamodb — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes internal object references—such as record IDs—and allows an authenticated subject to access or modify data without proper authorization checks. In a Feathersjs service backed by DynamoDB, this commonly manifests when an endpoint like /users/:id uses the route parameter directly as a key in a get or query call against DynamoDB without verifying that the authenticated user owns or is permitted to access that item.
Consider a typical Feathersjs service definition using feathers-dynamodb. If the service relies solely on the incoming params.id to fetch a record, an attacker who can obtain or guess valid IDs can iterate through them and read other users’ data. For example, a request to /users/123 with an attacker’s token might return another user’s profile if the service does not scope the query to the authenticated user’s identity. This becomes more nuanced when partition keys and sort keys are used: a service might use a composite key such as PK = USER#123 and SK = PROFILE. If the service reads params.id and constructs a key without ensuring it matches the authenticated subject’s user ID, the request retrieves an unintended item, resulting in IDOR.
DynamoDB itself does not enforce application-level ownership; it returns the item if the key matches. Therefore, the responsibility to enforce ownership and access control lies with the Feathersjs service logic. Without explicit checks—such as comparing the authenticated user ID from the token (available in params.user when configured with authentication) with the ID embedded in the key—an attacker can traverse records by incrementing IDs or enumerating known identifiers. Real-world attack patterns include horizontal IDOR (accessing same-privilege accounts) and vertical IDOR (escalating to admin records). The OWASP API Security Top 10 lists IDOR as a prevalent risk, and in serverless contexts, misconfigured DynamoDB key schemas amplify the impact by making traversal straightforward.
Additionally, secondary effects emerge when IDOR is combined with other unchecked inputs. If the service allows filtering or searching via query parameters that map to DynamoDB scan or query conditions, an attacker might leverage injection or parameter pollution to widen the scope of accessible data. For instance, an endpoint that accepts a status filter must still scope the query to the user’s partition key; otherwise, an attacker could supply arbitrary values to enumerate records beyond their scope. This underscores the need to bind every data access operation to the requester’s identity and validate inputs against the expected schema, ensuring that only intended records are retrieved or modified.
Dynamodb-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring every DynamoDB access is scoped to the authenticated subject and validated against the application’s authorization model. In Feathersjs, this means explicitly using the authenticated user’s identity from params.user to construct keys and adding checks before performing get, create, update, or remove operations.
Below are concrete, syntactically correct examples for a Feathersjs service using feathers-dynamodb. The examples assume the service is configured with authentication that populates params.user.id (or params.user.sub) and that the DynamoDB table uses a composite key where the partition key encodes the user identifier.
Secure get by ID
Instead of directly using params.id, derive the partition key from the authenticated user and compare it with the requested ID. If they do not match, throw an error.
const { Forbidden } = require('@feathersjs/errors');
app.service('users').hooks({
before: {
get: [context => {
const { id } = context.params.id; // e.g., 'USER#123|PROFILE'
const userIdentity = context.params.user.id; // authenticated user ID
// Ensure the requested key belongs to the authenticated user
if (!id.startsWith('USER#' + userIdentity)) {
throw new Forbidden('Access denied: invalid object reference');
}
// Optionally enforce additional constraints on sort key or attributes
return context;
}]
}
});
Secure query with scoped partition key
When querying, always set the partition key to the authenticated user’s key and avoid allowing raw user input to dictate the key condition. Use DynamoDB’s query with a KeyConditionExpression that binds to the user-specific partition key.
const { Forbidden } = require('@feathersjs/errors');
app.service('user-items').hooks({
before: {
find: [context => {
const userId = context.params.user.id;
// Build the partition key from the authenticated user
const partitionKey = 'USER#' + userId;
// Optionally allow a sort key filter but ensure it’s scoped
const sortKeyCondition = context.params.query.sortKeyCondition || begins_with(sortKeyAttr, sortKeyPrefix);
// Explicitly set query parameters to avoid scanning the entire table
context.params.query = {
...context.params.query,
partitionKey,
// Ensure KeyConditionExpression or equivalent is used per adapter
keyCondition: { attributeName: 'pk', value: partitionKey }
};
return context;
}]
}
});
Secure create/update/remove with ownership check
For write operations, verify that any identifier provided by the client matches the authenticated user’s scope, or generate server-side identifiers to avoid reliance on client-supplied keys.
app.service('user-settings').hooks({
before: {
create: [context => {
const userId = context.params.user.id;
// If the client provides an ID, ensure it matches the user
if (context.data.id && context.data.id !== userId) {
throw new Forbidden('Cannot create resource for another user');
}
// Assign the server-side ID to enforce ownership
context.data.id = userId;
context.data.pk = 'USER#' + userId;
return context;
}],
update: [context => {
const userId = context.params.user.id;
const { id } = context.params.query; // or from params.id for direct updates
if (id && !id.startsWith('USER#' + userId)) {
throw new Forbidden('Cannot update resource owned by another user');
}
return context;
}],
remove: [context => {
const userId = context.params.user.id;
const { id } = context.params.query;
if (id && !id.startsWith('USER#' + userId)) {
throw new Forbidden('Cannot delete resource owned by another user');
}
return context;
}]
}
});
These patterns enforce that every DynamoDB operation is bound to the authenticated subject’s identity, preventing IDOR by design. They align with remediation guidance that emphasizes scoping, input validation, and explicit ownership checks rather than relying on the client-supplied identifier alone.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |