Insecure Direct Object Reference in Dynamodb
How Insecure Direct Object Reference Manifests in Dynamodb
Insecure Direct Object Reference (IDOR) in DynamoDB occurs when applications expose internal object identifiers (like DynamoDB keys) to users and trust them without proper authorization checks. This vulnerability is particularly dangerous in DynamoDB because the service's key-based access patterns make it easy to accidentally expose sensitive data.
The most common DynamoDB IDOR pattern involves exposing partition keys or sort keys directly in API endpoints. Consider a healthcare application where patient records are stored in a DynamoDB table with patient_id as the partition key. A vulnerable endpoint might look like:
app.get('/api/patients/:patientId', async (req, res) => {
const { patientId } = req.params;
const result = await dynamodb.get({
TableName: 'patients',
Key: { patient_id: patientId }
}).promise();
res.json(result.Item);
});This code trusts the patientId parameter without verifying that the authenticated user has access to that patient's records. An attacker can simply iterate through patient IDs to access any patient's data.
DynamoDB's flexible schema amplifies IDOR risks. Unlike relational databases with foreign key constraints, DynamoDB allows any attribute to be accessed if you have the key. This means IDOR vulnerabilities can expose not just the intended record but also any related data stored in the same table.
Another DynamoDB-specific IDOR pattern involves Global Secondary Indexes (GSIs). Applications often create GSIs for alternative access patterns, but if these indexes expose sensitive identifiers, attackers can bypass primary key protections. For example:
app.get('/api/patients-by-email/:email', async (req, res) => {
const { email } = req.params;
const result = await dynamodb.query({
TableName: 'patients',
IndexName: 'email-index',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: { ':email': email }
}).promise();
res.json(result.Items);
});If the email-index GSI exists, an attacker can enumerate emails to find valid patient records, even if they can't guess patient IDs.
DynamoDB's batch operations create additional IDOR surfaces. BatchGetItem and BatchWriteItem operations can be abused if they process user-supplied keys without validation:
app.post('/api/batch-patient-records', async (req, res) => {
const { patientIds } = req.body;
const keys = patientIds.map(id => ({ patient_id: id }));
const result = await dynamodb.batchGet({
RequestItems: {
patients: { Keys: keys }
}
}).promise();
res.json(result.Responses.patients);
});This endpoint allows attackers to retrieve multiple patient records in a single request by providing a list of patient IDs.
Scan operations without proper filters represent another IDOR vector in DynamoDB. While scans are generally discouraged for performance reasons, they're sometimes used for administrative interfaces:
app.get('/api/patients', async (req, res) => {
const result = await dynamodb.scan({
TableName: 'patients'
}).promise();
res.json(result.Items);
});Without pagination limits or user-specific filtering, this exposes all patient records to anyone who can access the endpoint.
Dynamodb-Specific Detection
Detecting IDOR in DynamoDB requires both static code analysis and dynamic scanning approaches. Static analysis should focus on identifying patterns where DynamoDB keys are exposed in API endpoints without proper authorization checks.
Key detection patterns include:
- API endpoints that accept DynamoDB partition keys or sort keys as parameters
- Code that directly passes user input to DynamoDB's Key parameter without validation
- Batch operations (BatchGetItem, BatchWriteItem) that process user-supplied keys
- Query operations on Global Secondary Indexes that expose sensitive identifiers
Dynamic scanning with middleBrick specifically tests for DynamoDB IDOR vulnerabilities by:
- Analyzing API endpoints for patterns that suggest DynamoDB key exposure
- Testing whether endpoints properly validate user permissions before accessing DynamoDB records
- Checking for consistent error responses that might reveal valid keys through timing or error message differences
- Verifying that batch operations properly validate all keys in the request
middleBrick's DynamoDB-specific detection includes testing for common IDOR patterns in serverless applications, where DynamoDB is frequently used. The scanner examines API Gateway endpoints, Lambda functions, and the DynamoDB operations they perform to identify potential authorization bypasses.
For manual testing, you can use tools like Burp Suite or curl to test for IDOR by:
- Modifying partition key values in API requests to access other users' data
- Testing batch operations with multiple user IDs to see if permissions are properly enforced
- Checking error responses for information leakage about valid keys
Consider this vulnerable DynamoDB query that middleBrick would flag:
const getDocument = async (userId, documentId) => {
const result = await dynamodb.get({
TableName: 'documents',
Key: { user_id: userId, document_id: documentId }
}).promise();
return result.Item;
};middleBrick detects that this function trusts both userId and documentId parameters without verifying that the authenticated user owns the requested document.
The scanner also tests for IDOR in DynamoDB's conditional writes and transactions. For example:
app.put('/api/documents/:id', async (req, res) => {
const { id } = req.params;
const { content } = req.body;
await dynamodb.update({
TableName: 'documents',
Key: { document_id: id },
UpdateExpression: 'set content = :c',
ConditionExpression: 'attribute_exists(document_id)',
ExpressionAttributeValues: { ':c': content }
}).promise();
res.status(200).send();
});middleBrick identifies that this update operation lacks authorization checks and could allow unauthorized document modifications.
Dynamodb-Specific Remediation
Remediating DynamoDB IDOR vulnerabilities requires implementing proper authorization checks before any DynamoDB operation. The key principle is to never trust user-supplied keys and always verify permissions.
For single-item operations, use DynamoDB's built-in authorization patterns:
const getPatientRecord = async (userId, patientId) => {
// Verify user has access to this patient
const hasAccess = await verifyUserAccess(userId, patientId);
if (!hasAccess) {
throw new Error('Access denied');
}
const result = await dynamodb.get({
TableName: 'patients',
Key: { patient_id: patientId }
}).promise();
return result.Item;
};The verifyUserAccess function should check your application's access control rules, such as whether the user is the patient, a doctor in the same practice, or has been granted explicit permissions.
For batch operations, validate each key individually:
const batchGetPatientRecords = async (userId, patientIds) => {
// Verify access to all requested patients
const accessResults = await Promise.all(
patientIds.map(id => verifyUserAccess(userId, id))
);
if (accessResults.includes(false)) {
throw new Error('Access denied to one or more records');
}
const keys = patientIds.map(id => ({ patient_id: id }));
const result = await dynamodb.batchGet({
RequestItems: {
patients: { Keys: keys }
}
}).promise();
return result.Responses.patients;
};This ensures users can only access records they're authorized to view, even when requesting multiple records at once.
Use DynamoDB's condition expressions for write operations:
const updateDocument = async (userId, documentId, content) => {
// Verify ownership
const document = await dynamodb.get({
TableName: 'documents',
Key: { document_id: documentId }
}).promise();
if (document.Item.user_id !== userId) {
throw new Error('Access denied');
}
await dynamodb.update({
TableName: 'documents',
Key: { document_id: documentId },
UpdateExpression: 'set content = :c',
ConditionExpression: 'user_id = :userId',
ExpressionAttributeValues: {
':c': content,
':userId': userId
}
}).promise();
};The condition expression provides an additional layer of protection by ensuring the document belongs to the user attempting the update.
For query operations on GSIs, implement access control at the application level:
const getPatientsByEmail = async (userId, email) => {
// Verify user can search by email
if (!canUserSearchByEmail(userId)) {
throw new Error('Access denied');
}
const result = await dynamodb.query({
TableName: 'patients',
IndexName: 'email-index',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: { ':email': email }
}).promise();
// Filter results to only show accessible records
const accessibleRecords = result.Items.filter(
record => verifyUserAccess(userId, record.patient_id)
);
return accessibleRecords;
};This approach allows legitimate search functionality while preventing enumeration attacks.
Implement row-level security using DynamoDB's built-in features:
const getAccessibleRecords = async (userId, filterExpression) => {
const result = await dynamodb.query({
TableName: 'patients',
IndexName: 'user-access-index',
KeyConditionExpression: 'user_id = :userId',
FilterExpression: filterExpression,
ExpressionAttributeValues: { ':userId': userId }
}).promise();
return result.Items;
};This design pattern stores access control information in DynamoDB and uses it to filter queries, ensuring users only see records they're authorized to access.
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 |