Rate Limiting Bypass in Dynamodb
How Rate Limiting Bypass Manifests in Dynamodb
Rate limiting bypass in DynamoDB manifests through several API-specific patterns that exploit the service's request handling architecture. Unlike traditional databases where rate limiting is enforced at the application layer, DynamoDB implements provisioned throughput and adaptive capacity at the service level, creating unique bypass opportunities.
The most common bypass occurs through partition key manipulation. DynamoDB allocates read/write capacity uniformly across partition keys. When an application enforces rate limiting at the API level but doesn't validate partition key uniqueness, attackers can distribute requests across multiple partition keys to circumvent limits. For example:
// Vulnerable: No partition key validation
async function getItems(userId, limit) {
const keys = [];
for (let i = 0; i < limit; i++) {
keys.push({
userId: userId,
nonce: crypto.randomUUID() // Attacker controlled
});
}
const params = {
RequestItems: {
'Orders': {
Keys: keys
}
}
};
return await dynamodb.batchGet(params).promise();
}
An attacker can call this function with a high limit value, causing DynamoDB to process many requests across different partition keys, effectively bypassing any API-level rate limiting.
Conditional writes with predictable tokens represent another bypass vector. DynamoDB's ConditionExpression allows conditional updates, but if tokens are predictable (like sequential integers), attackers can race conditions:
// Vulnerable: Predictable token enables race conditions
async function createOrder(userId, orderId) {
const params = {
TableName: 'Orders',
Item: {
userId: userId,
orderId: orderId,
status: 'pending'
},
ConditionExpression: 'attribute_not_exists(orderId)'
};
try {
await dynamodb.put(params).promise();
return true;
} catch (err) {
if (err.code === 'ConditionalCheckFailedException') {
return false;
}
throw err;
}
}
Attackers can pre-generate sequential order IDs and attempt multiple creations simultaneously, potentially overwhelming the system before rate limits trigger.
Global Secondary Indexes (GSIs) create additional bypass opportunities. Applications often implement rate limiting on the base table but forget GSIs, allowing attackers to route requests through alternative access patterns:
// Vulnerable: Rate limiting on base table only
async function getOrdersByUserId(userId) {
const params = {
TableName: 'Orders',
IndexName: 'UserIdIndex',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': userId }
};
return await dynamodb.query(params).promise();
}
If the GSI has different provisioned capacity or adaptive capacity settings, requests can be shifted there to bypass primary table limits.
Dynamodb-Specific Detection
Detecting rate limiting bypass in DynamoDB requires monitoring specific patterns that generic API security tools miss. The DynamoDB-specific approach focuses on request distribution patterns, capacity utilization anomalies, and access pattern analysis.
Partition key distribution analysis reveals when requests are being artificially spread across keys. Monitor for:
# Detection: Analyze partition key distribution
import boto3
from collections import Counter
def analyze_partition_distribution(table_name):
client = boto3.client('dynamodb')
response = client.scan(
TableName=table_name,
Select='COUNT'
)
# Extract partition keys from items
partition_keys = [item['pk']['S'] for item in response['Items']]
key_counts = Counter(partition_keys)
# Flag if distribution is too uniform (potential bypass)
if len(key_counts) > 0.8 * len(partition_keys):
return {
'issue': 'Potential rate limiting bypass',
'description': 'Partition key distribution suggests artificial spreading',
'uniformity_score': len(key_counts) / len(partition_keys)
}
return None
Conditional write success rate monitoring identifies race condition exploitation:
# Detection: Monitor conditional write failures
def monitor_conditional_writes(table_name):
client = boto3.client('dynamodb')
response = client.describe_table(TableName=table_name)
# Check for unusual conditional check failure rates
metrics = cloudwatch_client.get_metric_statistics(
Namespace='AWS/DynamoDB',
MetricName='ConditionalCheckFailedRequests',
Dimensions=[{'Name': 'TableName', 'Value': table_name}],
StartTime=datetime.utcnow() - timedelta(hours=1),
EndTime=datetime.utcnow(),
Period=300,
Statistics=['SampleCount']
)
if metrics['Datapoints'] and metrics['Datapoints'][0]['SampleCount'] > threshold:
return {
'issue': 'Suspicious conditional write patterns',
'description': 'High rate of conditional check failures may indicate race conditions'
}
return None
Cross-index access pattern analysis detects when attackers shift requests between base tables and GSIs:
# Detection: Compare access patterns across indexes
def analyze_index_access(table_name):
client = boto3.client('dynamodb')
# Get provisioned capacity for base table and indexes
table_info = client.describe_table(TableName=table_name)
base_capacity = table_info['Table']['ProvisionedThroughput']
# Check if GSIs have significantly different capacity
for index in table_info['Table']['GlobalSecondaryIndexes']:
if index['ProvisionedThroughput']['ReadCapacityUnits'] != base_capacity['ReadCapacityUnits']:
return {
'issue': 'Index capacity mismatch',
'description': 'Index provisioned capacity differs from base table',
'risk': 'Potential rate limiting bypass through index switching'
}
return None
middleBrick's DynamoDB-specific scanning automatically tests these patterns by:
- Analyzing partition key generation patterns in API requests
- Testing conditional write race conditions with multiple concurrent requests
- Evaluating GSI access patterns and capacity mismatches
- Checking for predictable token generation in conditional operations
The scanner generates a risk score (0-100) with specific findings like "Partition key manipulation detected" or "Index capacity mismatch enabling bypass" along with remediation guidance.
Dynamodb-Specific Remediation
Remediating rate limiting bypass in DynamoDB requires leveraging the service's native features and implementing application-level controls that account for DynamoDB's distributed architecture.
Partition key validation and rate limiting prevents artificial distribution:
// Remediation: Validate partition key generation
const crypto = require('crypto');
// Generate cryptographically secure partition keys
function generateSecurePartitionKey(userId, maxKeys) {
const buffer = crypto.randomBytes(16);
const partitionKey = buffer.toString('hex');
return {
userId: userId,
partitionKey: partitionKey
};
}
// Rate limit based on actual DynamoDB capacity
async function getItemsWithRateLimiting(userId, limit, maxRequestsPerSecond) {
const params = {
TableName: 'Orders',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': userId },
Limit: limit
};
// Implement client-side rate limiting
const client = new DynamoDB.DocumentClient();
const startTime = Date.now();
let remaining = limit;
while (remaining > 0) {
const batchSize = Math.min(remaining, maxRequestsPerSecond);
const response = await client.query(params).promise();
// Process results
remaining -= batchSize;
// Rate limit based on DynamoDB's provisioned capacity
const elapsed = Date.now() - startTime;
const targetDelay = (batchSize / maxRequestsPerSecond) * 1000;
if (elapsed < targetDelay) {
await new Promise(resolve => setTimeout(resolve, targetDelay - elapsed));
}
}
return response;
}
Conditional write with atomic counters prevents race conditions:
// Remediation: Use atomic counters for sequential operations
async function createOrderAtomic(userId, orderId) {
const client = new DynamoDB.DocumentClient();
// Use transactional writes to ensure atomicity
const transactionItems = [
{
Put: {
TableName: 'Orders',
Item: {
userId: userId,
orderId: orderId,
status: 'pending'
},
ConditionExpression: 'attribute_not_exists(orderId)'
}
},
{
Update: {
TableName: 'OrderCounters',
Key: { userId: userId },
UpdateExpression: 'set nextOrderId = nextOrderId + :val',
ExpressionAttributeValues: { ':val': 1 },
ConditionExpression: 'nextOrderId = :expected'
}
}
];
try {
await client.transactWrite({
TransactItems: transactionItems
}).promise();
return true;
} catch (err) {
if (err.code === 'TransactionCanceledException') {
return false;
}
throw err;
}
}
Consistent capacity provisioning across indexes eliminates bypass through capacity differences:
// Remediation: Ensure consistent capacity across base table and indexes
async function provisionConsistentCapacity(tableName, readCapacity, writeCapacity) {
const client = new DynamoDB.DocumentClient();
// Provision base table
await client.updateTable({
TableName: tableName,
ProvisionedThroughput: {
ReadCapacityUnits: readCapacity,
WriteCapacityUnits: writeCapacity
}
}).promise();
// Provision all GSIs with matching capacity
const tableInfo = await client.describeTable({ TableName: tableName }).promise();
for (const index of tableInfo.Table.GlobalSecondaryIndexes) {
await client.updateTable({
TableName: tableName,
GlobalSecondaryIndexUpdates: [{
Update: {
IndexName: index.IndexName,
ProvisionedThroughput: {
ReadCapacityUnits: readCapacity,
WriteCapacityUnits: writeCapacity
}
}
}]
}).promise();
}
}
Implement DynamoDB-specific rate limiting with adaptive throttling:
// Remediation: Adaptive rate limiting based on DynamoDB metrics
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();
class DynamoDBRateLimiter {
constructor(tableName, baseRate) {
this.tableName = tableName;
this.baseRate = baseRate;
this.currentRate = baseRate;
this.lastThrottle = Date.now();
}
async getCurrentCapacity() {
const data = await cloudwatch.getMetricStatistics({
Namespace: 'AWS/DynamoDB',
MetricName: 'ConsumedReadCapacityUnits',
Dimensions: [{ Name: 'TableName', Value: this.tableName }],
StartTime: new Date(Date.now() - 300000),
EndTime: new Date(),
Period: 60,
Statistics: ['Average']
}).promise();
return data.Datapoints.length > 0 ? data.Datapoints[0].Average : this.baseRate;
}
async shouldThrottle(requestCount) {
const currentCapacity = await this.getCurrentCapacity();
const capacityRatio = currentCapacity / this.baseRate;
// Adjust rate based on current load
this.currentRate = Math.max(1, this.baseRate * (1 - capacityRatio * 0.5));
// Throttle if approaching capacity limits
if (requestCount > this.currentRate) {
const throttleTime = (requestCount - this.currentRate) / this.currentRate * 1000;
await new Promise(resolve => setTimeout(resolve, throttleTime));
return true;
}
return false;
}
}
middleBrick's remediation guidance** includes specific DynamoDB patterns like:
- "Implement partition key validation to prevent artificial distribution"
- "Use transactional writes instead of conditional writes for race condition prevention"
- "Ensure consistent provisioned capacity across base tables and GSIs"
- "Implement client-side rate limiting based on DynamoDB's adaptive capacity metrics"
The tool provides code snippets and configuration examples specific to DynamoDB's API and capacity model, helping developers implement these remediations effectively.
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |