Replay Attack in Grape with Cockroachdb
Replay Attack in Grape with Cockroachdb — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and retransmits it to reproduce an authorized action. In a Grape API backed by Cockroachdb, this risk emerges at the intersection of idempotent HTTP method usage, session or token handling, and Cockroachdb’s serializable isolation guarantees. Because Cockroachdb provides strong consistency and serializable transactions, it can detect write skew and aborted transactions, but it does not inherently prevent an attacker from re-submitting the same request body, identifiers, and authentication context if the API layer does not enforce replay protection.
Consider a payment endpoint implemented with Grape where a client submits a transaction with an idempotency key. If the key is not validated or stored atomically alongside the transaction state in Cockroachdb, an attacker can replay the same HTTP request with the same idempotency key, same payload, and same authenticated session. Cockroachdb’s serializable transactions may commit both attempts if the application does not enforce uniqueness constraints on the idempotency key, or if the key is checked and the transaction committed in separate steps without proper conflict resolution. This can lead to duplicate charges or state changes despite the database’s strong isolation, because the application logic, not the database, decides whether the request is a duplicate.
Grape’s middleware and routing can inadvertently expose replay risks when authentication headers or tokens are accepted without additional context such as timestamps or nonces. For example, a POST /transactions endpoint that relies solely on bearer token authentication and does not include request hashing or one-time-use markers allows captured requests to be replayed within the token’s validity window. Cockroachdb will faithfully persist the resulting writes, and its distributed nature means that writes observed in one region may be quickly visible across nodes, but this visibility does not equate to replay protection. From an OWASP API Top 10 perspective, this aligns with broken object level authorization and insufficient idempotency controls, where the API fails to ensure that a valid request cannot be maliciously repeated with the same effect.
In a real-world scenario, an unauthenticated LLM endpoint or an exported OpenAPI spec might inadvertently document idempotency key usage without clarifying that the backend must enforce uniqueness at the database level. If the Grape API parses the spec and generates routes that assume idempotency keys are enforced by the client alone, replay becomes feasible. Additionally, SSRF-related risks can compound this if an internal service used by Grape is co-located with Cockroachdb and lacks network-level protections, allowing an attacker to leverage internal reachability to replay transactional requests. The key takeaway is that Cockroachdb’s strong consistency supports safe state transitions only when the API layer couples requests with verifiable, server-side controls such as stored idempotency keys, timestamps, or cryptographic nonces that survive serializable transaction aborts.
Cockroachdb-Specific Remediation in Grape — concrete code fixes
To mitigate replay attacks in a Grape API using Cockroachdb, implement server-side idempotency enforcement with uniqueness constraints and conditional writes. This requires storing idempotency keys in a dedicated table with a unique constraint and resolving conflicts within a Cockroachdb serializable transaction. The following code examples assume a Grape API resource for transactions and use the pg gem to interact with Cockroachdb.
1. Schema and unique constraint
Create an idempotency table that references the transaction and enforces uniqueness on the client-supplied key.
CREATE TABLE IF NOT EXISTS idempotency_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key TEXT NOT NULL UNIQUE,
transaction_id UUID NOT NULL REFERENCES transactions(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
2. Idempotent transaction endpoint in Grape
Within a Grape resource, use a serializable transaction to check and insert the idempotency key. If a uniqueness violation occurs, retrieve the existing transaction instead of creating a new one.
require 'pg'
require 'json'
class TransactionResource < Grape::API
format :json
helpers do
def with_cockroachdb
conn = PG.connect(ENV['DATABASE_URL'])
conn.transaction do
yield(conn)
end
rescue PG::UniqueViolation
# Idempotency key already used; safe to read the existing record
raise
end
end
post '/transactions' do
payload = JSON.parse(request.body.read)
idempotency_key = request.env['HTTP_Idempotency-Key']
amount = payload['amount']
account_id = payload['account_id']
result = with_cockroachdb do |conn|
# Serializable isolation ensures no concurrent duplicate commits
conn.exec_params('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE')
# Try to insert the idempotency key; fail if key exists
res = conn.exec_params(<<~SQL, [idempotency_key, account_id, amount])
INSERT INTO idempotency_keys (key, transaction_id)
VALUES ($1, transactions.id)
ON CONFLICT (key) DO NOTHING
RETURNING idempotency_keys.id, transactions.id AS transaction_id, transactions.amount, transactions.status
SQL
if res.ntuples > 0
# Key was new; create transaction and link via idempotency_keys
tx_res = conn.exec_params(<<~SQL, [account_id, amount])
INSERT INTO transactions (account_id, amount, status)
VALUES ($1, $2, 'completed')
RETURNING id, amount, status
SQL
tx = tx_res[0]
conn.exec_params(<<~SQL, [idempotency_key, tx['id']]
INSERT INTO idempotency_keys (key, transaction_id)
VALUES ($1, $2)
SQL, [idempotency_key, tx['id']])
tx
else
# Key existed; fetch the original transaction
existing = conn.exec_params(<<~SQL, [idempotency_key])
SELECT t.id, t.amount, t.status
FROM transactions t
JOIN idempotency_keys ik ON t.id = ik.transaction_id
WHERE ik.key = $1
SQL, [idempotency_key]
existing[0]
end
end
present result, status: 200
end
end
This pattern ensures that the key uniqueness check and transaction creation occur within a single serializable transaction block. If two identical requests arrive concurrently, Cockroachdb’s serializable isolation will cause one transaction to abort with a serialization error; the API can catch that and retry or return the already-committed result, effectively preventing duplicate processing.
3. Include timestamp or nonce for additional replay protection
When idempotency keys are not feasible, include a timestamp and reject requests with stale timestamps. Combine with short-lived tokens and server-side storage of recent nonces to defend against windowed replays.
helpers do
def reject_replayed_request(timestamp, nonce, tolerance: 30, seen_window: 300)
# Reject if timestamp is outside acceptable window
now = Time.now.utc.to_i
return false if (now - timestamp).abs > tolerance
# Store nonce in Cockroachdb with expiration; reject duplicates
conn = PG.connect(ENV['DATABASE_URL'])
inserted = conn.exec_params(<<~SQL, [nonce, Time.now.utc + seen_window])
INSERT INTO nonces (nonce, expires_at)
VALUES ($1, $2)
ON CONFLICT (nonce) DO NOTHING
SQL
inserted.cmd_tuples.zero? ? false : true
end
end
By combining Cockroachdb’s strong consistency with explicit idempotency or nonce checks, Grape APIs can safely reject replayed requests while maintaining correctness under high concurrency.