Double Free in Feathersjs with Dynamodb
Double Free in Feathersjs with Dynamodb — how this specific combination creates or exposes the vulnerability
Double Free is a memory safety issue that typically arises when a program deallocates the same memory region twice. In the context of Feathersjs with DynamoDB, the risk is not low-level memory corruption but logical double-release style bugs: performing two side-effectful operations that both mutate or remove the same record, triggered by a single client request. This can occur when a Feathers service hook chain or custom method handler executes multiple conditional write paths, such as a direct DynamoDB delete combined with a secondary update or a conditional check that both attempt to change the same item.
Feathersjs is a framework that encourages a service-oriented architecture where hooks centralize business logic. When a service uses the AWS SDK to interact with DynamoDB, hooks may implement authorization, validation, or soft-delete patterns. If two hooks or two branches within a single hook both call delete or update operations on the same DynamoDB key without idempotency guards or transactional safeguards, the effective behavior can resemble a double-free scenario: the record is deleted, and a subsequent operation attempts to act on an already-absent item, leading to inconsistent state or unexpected errors returned to the client.
The DynamoDB API itself is eventually consistent within a region and does not inherently prevent double operations; it will process each write independently. For example, a delete followed by an update on the same key may result in a ConditionalCheckFailedException if a condition is used, or silently succeed on the delete and then fail on the update, depending on how the keys and conditions are structured. A Feathersjs hook that calls delete and then another hook calls update, both responding to the same event, can produce a partial failure state that resembles a double-free in its impact—corrupting the expected data flow and potentially exposing internal logic through error messages.
Consider a Feathers service that manages user profiles stored in DynamoDB. A before hook might enforce ownership by checking a user ID; an after hook might send a notification and also attempt to clean up related metadata. If the before hook deletes the profile and the after hook tries to update the same profile’s metadata, the second operation will reference a missing item. Without proper idempotency keys or transaction-like coordination via conditional expressions, this sequence can produce duplicate error responses or inconsistent audit trails, effectively a logical double-free in the application layer.
To mitigate this class of issue, design hooks to be idempotent and ensure only one mutating operation per logical action. Use DynamoDB conditional writes to enforce state transitions atomically, and avoid chaining multiple delete or update calls on the same key within a single request lifecycle. The framework’s hook architecture should centralize mutation logic or use a unit of work pattern to prevent overlapping operations.
Dynamodb-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring that only one mutating operation affects a given DynamoDB item per request. Use conditional expressions to make updates or deletes atomic, and structure Feathers hooks to avoid multiple writes to the same key. Below are concrete code examples for Feathersjs services using the AWS SDK for DynamoDB.
First, configure the DynamoDB client in your Feathers app:
const { DynamoDB } = require('aws-sdk');
const ddb = new DynamoDB({ region: 'us-east-1' });
Define a Feathers service that uses a single conditional delete to remove a record only if it is in an expected state, preventing a second operation from acting on an already-deleted item:
app.use('/profiles', {
async remove(id, params) {
const params = {
TableName: 'profiles',
Key: { id: { S: id } },
ConditionExpression: 'attribute_exists(id) AND #state = :active',
ExpressionAttributeNames: { '#state': 'state' },
ExpressionAttributeValues: { ':active': { S: 'active' } }
};
try {
await ddb.deleteItem(params).promise();
return { id, deleted: true };
} catch (error) {
if (error.code === 'ConditionalCheckFailedException') {
throw new errors.GeneralError('Cannot delete; item may already be removed or in an invalid state', { code: 409 });
}
throw error;
}
}
});
For updates, use a conditional write to ensure the item has not been concurrently modified by another process, avoiding a second write attempt that could act on stale state:
app.use('/profiles', {
async patch(id, data, params) {
const updateParams = {
TableName: 'profiles',
Key: { id: { S: id } },
UpdateExpression: 'set #name = :name, #email = :email',
ConditionExpression: 'attribute_exists(id)',
ExpressionAttributeNames: { '#name': 'name', '#email': 'email' },
ExpressionAttributeValues: {
':name': { S: data.name },
':email': { S: data.email }
},
ReturnValues: 'ALL_NEW'
};
try {
const result = await ddb.updateItem(updateParams).promise();
return result.Attributes;
} catch (error) {
if (error.code === 'ConditionalCheckFailedException') {
throw new errors.GeneralError('Concurrent modification detected', { code: 409 });
}
throw error;
}
}
});
In Feathers hooks, ensure that only one hook category (before, after, error) performs the write. If a before hook deletes an item, set a flag in params to skip after hooks that would attempt a secondary write:
app.service('profiles').hooks({
before: {
async remove(hook) {
const deleteParams = {
TableName: 'profiles',
Key: { id: { S: hook.id } },
ConditionExpression: 'attribute_exists(id)'
};
try {
await ddb.deleteItem(deleteParams).promise();
hook.params.deletedProfile = true;
} catch (error) {
if (error.code === 'ConditionalCheckFailedException') {
throw new errors.GeneralError('Already deleted', { code: 409 });
}
throw error;
}
}
},
after: {
async remove(hook) {
if (hook.params.deletedProfile) {
return;
}
// Safe to proceed with non-mutating logic like notifications
}
}
});
Use DynamoDB transactions when multiple related items must be modified atomically, preventing partial updates that can resemble double operations:
app.use('/orders', {
async create(data, params) {
const transactItems = [
{
Put: {
TableName: 'orders',
Item: {
orderId: { S: data.orderId },
status: { S: 'pending' }
},
ConditionExpression: 'attribute_not_exists(orderId)'
}
},
{
Update: {
TableName: 'accounts',
Key: { accountId: { S: data.accountId } },
UpdateExpression: 'ADD balance :debit',
ExpressionAttributeValues: { ':debit': { N: `-${data.amount}` } },
ConditionExpression: 'balance >= :debit'
}
}
];
const result = await ddb.transactWriteItems({ TransactItems: transactItems }).promise();
return result;
}
});
These patterns reduce the chance of double-write or double-delete style issues by enforcing atomicity and idempotency at the DynamoDB layer and structuring Feathers hooks to avoid redundant operations on the same resource.