Replay Attack in Grape with Dynamodb
Replay Attack in Grape with Dynamodb — how this specific combination creates or exposes the vulnerability
A replay attack in a Ruby Grape API backed by DynamoDB occurs when an attacker captures a valid request—typically including an authentication token, timestamp, and nonce—and re-submits it to the same endpoint to gain unauthorized access or cause duplicate operations. Because Grape is a REST-focused framework and DynamoDB is a fast key-value store, the interaction between idempotent client-side logic and DynamoDB’s conditional writes can inadvertently enable replayed requests to succeed when protections are missing.
The vulnerability surface arises when:
- The API does not enforce strict one-time use for critical operations (such as payments or state changes).
- Timestamp or nonce checks are performed in application logic but not validated atomically with DynamoDB writes.
- Conditional writes in DynamoDB use weak conditions (e.g., checking existence of a record) that an attacker can bypass by replaying the same condition after a previous legitimate write has already changed state.
For example, consider a payment endpoint that accepts order_id and amount. If the client sends a request with a timestamp and a unique request ID, and the server only checks that the request ID does not already exist in DynamoDB before inserting a new record, a replayed request with the same ID will be rejected—provided the conditional write is correctly implemented. However, if the condition is too permissive (for instance, using attribute_not_exists(order_id) but the client can vary other idempotency fields like user ID only), an attacker might replay with a different user context and still succeed if authorization boundaries are not enforced at the resource level.
Additionally, without per-request nonces or cryptographic signatures, an intercepted request can be replayed within the timestamp skew window. DynamoDB Streams can amplify risk if downstream consumers process replayed writes without additional deduplication, leading to duplicate side effects such as double charges or inventory decrements. Therefore, replay protection must combine strong idempotency keys, server-side validation, and atomic DynamoDB conditional logic to ensure that each operation is unique and tied to the correct authorization context.
Dynamodb-Specific Remediation in Grape — concrete code fixes
To mitigate replay attacks in Grape with DynamoDB, implement idempotency keys, conditional writes, and server-side timestamp/nonce validation. Below are concrete, working examples using the official AWS SDK for Ruby (v3).
1. Idempotency table design: Use a dedicated DynamoDB table to store request identifiers with a TTL to avoid indefinite storage growth.
require 'aws-sdk-dynamodb'
client = Aws::DynamoDB::Client.new(region: 'us-east-1')
# Ensure idempotency table exists with a primary key `id` (String) and TTL enabled
table_name = 'api_idempotency'
client.create_table({
table_name: table_name,
key_schema: [{ attribute_name: 'id', key_type: 'HASH' }],
attribute_definitions: [{ attribute_name: 'id', attribute_type: 'S' }],
billing_mode: 'PAY_PER_REQUEST',
ttl_specification: { enabled: true, attribute_name: 'ttl' }
}) unless client.list_tables(table_names: [table_name]).table_names.include?(table_name)
2. Idempotent payment endpoint in Grape with atomic conditional write:
class PaymentResource
include Grape::API
helpers do
def idempotency_key
env['HTTP_X_IDEMPOTENCY_KEY'] || SecureRandom.uuid
end
def current_user
# Assume authentication sets current_user
end
end
desc 'Create a payment', idempotency: true
params do
requires :order_id, type: String, desc: 'Unique order identifier'
requires :amount, type: Float, desc: 'Payment amount'
end
post '/payments' do
key = idempotency_key
user_id = current_user.id
timestamp = Time.now.utc.iso8601
table = Aws::DynamoDB::Table.new('api_idempotency')
payment_table = Aws::DynamoDB::Table.new('payments')
# Try to record idempotency atomically
idemp_put = table.put_item({
item: {
id: key,
user_id: user_id,
created_at: timestamp,
ttl: (Time.now.utc + 30 * 24 * 60 * 60).to_i # 30 days TTL
},
condition_expression: 'attribute_not_exists(id)'
})
# If idempotency record already exists, fetch the original response
if idemp_put.successful?
# Proceed with payment logic only once
payment_table.put_item({
item: {
payment_id: SecureRandom.uuid,
user_id: user_id,
order_id: params[:order_id],
amount: params[:amount],
created_at: timestamp
},
condition_expression: 'attribute_not_exists(payment_id)'
})
{ status: 'success', idempotency_key: key }
else
# Conflict: retrieve the previous response if you store it, or return 409
error!({ error: 'Duplicate request' }, 409)
end
end
end
3. Enforce server-side timestamp window and nonce to prevent out-of-window replays:
helpers do
def validate_request_nonce(nonce, timestamp)
# Reject if timestamp is outside allowed skew (e.g., 5 minutes)
allowed_skew = 300 # seconds
request_time = Time.iso8601(timestamp)
now = Time.now.utc
return false if (now - request_time).abs > allowed_skew
# Ensure nonce has not been used within the skew window
table = Aws::DynamoDB::Table.new('api_nonces')
# Conditional write to record nonce atomically
begin
table.put_item({
item: { nonce: nonce, used_at: now.iso8601 },
condition_expression: 'attribute_not_exists(nonce)'
})
true
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
false
end
end
end
4. Use strong authorization checks per resource to ensure a replayed request cannot act as another user even if the idempotency key differs:
# In your endpoint, always scope writes by user_id
params do
requires :user_id, type: String, desc: 'User identifier from auth'
end
post '/orders' do
user_id = params[:user_id]
# Ensure the authenticated user matches the user_id in params
error!('Forbidden', 403) unless user_id == current_user.id
# DynamoDB conditional write scoped to user_id
table = Aws::DynamoDB::Table.new('user_orders')
table.put_item({
item: {
user_id: user_id,
order_id: SecureRandom.uuid,
created_at: Time.now.utc.iso8601
},
condition_expression: 'attribute_not_exists(order_id)'
})
{ status: 'created' }
end
By combining unique idempotency keys, conditional DynamoDB writes, timestamp nonces, and strict user scoping, you reduce the risk of successful replay attacks while maintaining compatibility with DynamoDB’s performance characteristics.