Replay Attack in Grape with Hmac Signatures
Replay Attack in Grape with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and re-sends it to the server to produce an unintended effect. In Grape APIs that use HMAC signatures for request authentication, the vulnerability arises when the server validates the signature but does not enforce strict request uniqueness. HMAC signatures are computed over selected parts of the request—typically the HTTP method, path, timestamp, and body—so the same inputs will produce the same signature. If a server accepts a request with a valid HMAC but does not ensure freshness or uniqueness, an attacker can capture a legitimate signed request and replay it at a later time, potentially gaining access or causing repeated actions.
Consider a Grape endpoint that accepts a POST with an X-API-Timestamp and an X-API-Signature. The signature is generated with a shared secret over the concatenation of method, request path, timestamp, and body. If the server only checks that the signature matches and that the timestamp is not too old (for example, within five minutes), it might still accept a valid, previously seen request if the timestamp falls within the allowed window. Without a nonce or a server-side record of recently used timestamp+signature pairs, the server cannot distinguish a legitimate fresh request from a replayed one. This is especially risky for operations that change state, such as creating a resource or charging a payment, because the replayed request will be processed as if it were a new, legitimate action.
The risk is compounded when the timestamp granularity is coarse or when clocks between client and server are not tightly synchronized. An attacker can slightly adjust timestamps within the allowed skew window and still produce a valid HMAC if the server does not enforce strict one-time use. Additionally, if the request body is large and only partially covered by the signature, or if the signature is computed over a subset of headers, an attacker might modify non-covered parameters between replays. Because HMAC signatures provide integrity and authenticity but not replay protection by themselves, the server must explicitly incorporate mechanisms such as single-use timestamps or cryptographic nonces to prevent this class of attack.
Hmac Signatures-Specific Remediation in Grape — concrete code fixes
To mitigate replay attacks in Grape when using HMAC signatures, you should ensure that each request includes a unique, one-time component and that the server tracks or validates that component within a short window. A common approach is to use a timestamp combined with a nonce or to maintain a short-lived cache of recently seen signature components. Below are concrete code examples showing how to implement server-side verification that rejects replays.
First, a helper to maintain a short-term set of used nonce values. In a production environment, replace the in-memory Set with a distributed cache with TTL to support multi-instance deployments.
require 'set'
require 'time'
module NonceStore
@used_nonces = Set.new
@mutex = Mutex.new
TTL = 300 # seconds, slightly larger than the request timestamp window
def self.add(nonce)
@mutex.synchronize do
@used_nonces.add(nonce)
end
end
def self.include?(nonce)
@mutex.synchronize do
@used_nonces.include?(nonce)
end
end
def self.purge_expired(threshold_time)
@mutex.synchronize do
@used_nonces.select! { |n| n >= threshold_time }
end
end
end
Next, configure your Grape endpoint to require and validate the timestamp and nonce. The example below shows a before hook that extracts X-API-Timestamp, X-API-Nonce, and X-API-Signature, then verifies the signature and ensures the nonce has not been used recently.
class App < Grape::API
format :json
helpers do
def verify_hmac_signature!
timestamp = request.headers['X-API-Timestamp']
nonce = request.headers['X-API-Nonce']
signature = request.headers['X-API-Signature']
return error!('Missing authentication headers', 401) if timestamp.nil? || nonce.nil? || signature.nil?
# Reject stale requests to limit replay window
request_time = Time.parse(timestamp)
allowed_skew = 60 # seconds
now = Time.now.utc
if (now - request_time).abs > allowed_skew
error!('Request timestamp out of allowed skew', 401)
end
# Reject replayed nonces
if NonceStore.include?(nonce)
error!('Replay detected: nonce already used', 401)
end
# Compute expected signature (example uses a subset of headers and body)
message = "#{request.request_method}\n#{request.path}\n#{timestamp}\n#{nonce}\n#{request.body.read}"
expected_signature = OpenSSL::HMAC.hexdigest('sha256', ENV['HMAC_SECRET'], message)
request.body.rewind
unless Rack::Utils.secure_compare(signature, expected_signature)
error!('Invalid signature', 401)
end
NonceStore.add(nonce)
end
end
before { verify_hmac_signature! }
resource :orders do
desc 'Create an order',
headers: [
{ key: 'X-API-Timestamp', type: String, desc: 'ISO8601 timestamp' },
{ key: 'X-API-Nonce', type: String, desc: 'Unique nonce' },
{ key: 'X-API-Signature', type: String, desc: 'HMAC signature' }
],
http_codes: [
{ code: 201, message: 'Order created' },
{ code: 401, message: 'Unauthorized' }
]
post do
# Your order creation logic here
{ order_id: SecureRandom.uuid }
end
end
end
In this setup, the server rejects requests with timestamps outside an allowed skew and rejects any nonce that has already been seen within the TTL. The nonce can be a random value provided by the client (included in the signature base) or a server-generated value returned in a prior step. By combining HMAC signature verification with nonce or replay tracking, you effectively prevent an attacker from reusing captured requests even if they possess a valid signature.