Broken Access Control in Hapi with Dynamodb
Broken Access Control in Hapi with Dynamodb — how this specific combination creates or exposes the vulnerability
Broken Access Control in a Hapi application that uses DynamoDB typically arises when authorization checks are missing, incomplete, or bypassed before issuing DynamoDB operations. Hapi provides routing and request handling but does not enforce data-level permissions by default; developers must explicitly add authorization logic for each endpoint that interacts with DynamoDB. Without these checks, an authenticated user can manipulate request parameters such as path or query identifiers to access or modify resources that belong to other users.
For example, consider a route defined as /users/{userId}/profile. If the handler retrieves the profile by directly using request.params.userId to build a DynamoDB GetItem or Query request, and does not verify that the authenticated subject owns that userId, the endpoint becomes vulnerable to Broken Access Control (BOLA/IDOR). An attacker who knows another user’s ID can read or overwrite sensitive profile data. This risk is compounded when the DynamoDB table uses a composite key (partition key and sort key) and the application uses only a partial key derived from user input without validating ownership.
DynamoDB-specific factors amplify the impact. Because DynamoDB is a managed NoSQL store, queries are fast and flexible; if the application uses Scan or Query without a proper filter tied to the authenticated user’s identity, it might inadvertently expose other users’ items. A missing or misconfigured index can also cause the application to perform a full-table scan to locate a single user’s data, which is inefficient and may expose more data than intended. Additionally, if the application uses IAM roles or shared credentials with broad permissions (e.g., dynamodb:Query on the entire table) and does not scope data access per request, a compromised token or misconfigured role can lead to wide-ranging data exposure.
Common root causes include missing ownership checks, trusting client-supplied identifiers without server-side validation, and incorrectly assuming that frontend-enforced restrictions are sufficient. For instance, a client may send a userId in the payload or URL; if the backend uses that value directly to construct a KeyConditionExpression or FilterExpression without confirming it matches the authenticated subject, the access control boundary is broken. The combination of Hapi’s flexible routing and DynamoDB’s low-level API shifts responsibility fully to the developer to enforce correct authorization at the item and attribute level, following frameworks such as OWASP API Security Top 10 A01:2023.
Dynamodb-Specific Remediation in Hapi — concrete code fixes
To remediate Broken Access Control in Hapi with DynamoDB, enforce ownership checks at the handler level, scope all DynamoDB operations to the authenticated subject, and avoid trusting client-supplied identifiers for key construction. Below are concrete code examples that demonstrate these practices.
First, derive the user’s DynamoDB key from the authenticated subject rather than from request parameters. Assume your table uses PK as the partition key (e.g., USER#user-id) and SK as the sort key (e.g., PROFILE#self). Use the authenticated user ID to build these key values and ensure the request is targeting owned data.
const Hapi = require('@hapi/hapi');
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient({ region: 'us-east-1' });
const server = Hapi.server({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/users/{userId}/profile',
options: {
auth: 'session', // example auth strategy
handler: async (request, h) => {
const authenticatedUserId = request.auth.credentials.userId; // subject from auth
const requestedUserId = request.params.userId;
// Enforce ownership: do not proceed if IDs mismatch
if (authenticatedUserId !== requestedUserId) {
throw Boom.forbidden('Access denied: insufficient permissions');
}
const pk = `USER#${authenticatedUserId}`;
const sk = 'PROFILE#self';
const command = new GetItemCommand({
TableName: process.env.TABLE_NAME,
Key: {
pk: { S: pk },
sk: { S: sk }
}
});
const response = await client.send(command);
if (!response.Item) {
throw Boom.notFound('Profile not found');
}
return response.Item;
}
}
});
For queries that may return multiple items scoped to a user (e.g., user-owned posts), use the authenticated user ID as the partition key and avoid filtering on client-provided identifiers. If you must support secondary indexes, ensure the index’s partition key includes the user identifier and scope access accordingly.
const { DynamoDBClient, QueryCommand } = require('@aws-sdk/client-dynamodb');
server.route({
method: 'GET',
path: '/users/{userId}/posts',
options: {
auth: 'session',
handler: async (request, h) => {
const authenticatedUserId = request.auth.credentials.userId;
const requestedUserId = request.params.userId;
if (authenticatedUserId !== requestedUserId) {
throw Boom.forbidden('Access denied: insufficient permissions');
}
const command = new QueryCommand({
TableName: process.env.TABLE_NAME,
IndexName: 'gsi-user-timestamp',
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: {
':pk': { S: `USER#${authenticatedUserId}` }
}
});
const response = await client.send(command);
return response.Items;
}
}
});
Always validate and sanitize any user-controlled input used in key expressions or filter conditions. Avoid using client-supplied values directly as key components. Combine these checks with a robust authentication strategy and, where applicable, use DynamoDB condition expressions for additional write-time safety. These steps reduce the attack surface for Broken Access Control and help align the Hapi + DynamoDB stack with secure-by-design principles.