Replay Attack in Grape
How Replay Attack Manifests in Grape
In Grape, a Replay Attack exploits the absence of request uniqueness guarantees, typically within authentication or state-changing operations. A valid, captured request (e.g., a fund transfer or password change) is resent verbatim to achieve the same effect again. This is distinct from parameter tampering; the attacker reuses legitimate, signed credentials.
Grape's common authentication patterns create specific vulnerable surfaces:
- Stateless Token Auth Without Nonces: Many Grape APIs use JWT or API keys passed in headers (
Authorization: Bearer <token>). If the token validation logic (often in abeforeblock or helper) only verifies signature and expiry but does not check a request-specific nonce or timestamp, the token can be reused until it expires. - Missing Request Timestamp Validation: A Grape endpoint might accept a
timestampparameter but fail to validate it is recent (e.g., within 5 minutes). An attacker can capture a legitimate request and replay it hours later if the timestamp is ignored or not checked against a server-side window. - Idempotency Key Misuse: While idempotency keys prevent duplicate processing, Grape apps sometimes implement them incorrectly. If the key is client-controlled and stored without server-side expiration or scope binding (e.g., per user), an attacker can replay the same key with different payloads or reuse an old key after the intended operation succeeded.
Vulnerable Grape Code Pattern:
module API
class Transactions < Grape::API
before { authenticate! }
post '/transfer' do
# VULNERABLE: No nonce, timestamp, or idempotency check
from = params[:from_account]
to = params[:to_account]
amount = params[:amount]
Bank.transfer(from, to, amount)
{ status: 'success' }
end
end
endHere, any valid authenticated request to /transfer can be replayed indefinitely. An attacker intercepting a request with a valid session token can resend it to repeatedly drain an account.
Grape-Specific Detection
Manual Detection: Review Grape's authentication helpers and before filters. Look for:
- Absence of a nonce generation/validation step. A secure pattern would generate a cryptographically random nonce per request, store it server-side (e.g., Redis) with a short TTL, and reject duplicates.
- Missing or incorrect timestamp validation. Check if the code parses a
X-Request-Timestampheader and rejects requests older than, say, 300 seconds. - Idempotency key implementation that only checks existence without validating the key's association with the specific user/action and without expiring old keys.
Test manually by capturing a legitimate request (e.g., with Burp Suite or OWASP ZAP) and resending it after a delay or from a different session. If the server processes it again identically, the endpoint is vulnerable.
Automated Detection with middleBrick: middleBrick's Authentication and Input Validation checks actively probe for replay vulnerabilities in Grape APIs. During a scan, it:
- Extracts the authentication mechanism (e.g., Bearer token, API key) from the OpenAPI spec or runtime responses.
- Issues a valid authenticated request and records the response, including any anti-replay headers (e.g.,
Idempotency-Key,Nonce) or timestamp fields. - Attempts to replay the identical request (same headers, body, method) multiple times. If subsequent replays return
200 OKor similar success codes instead of409 Conflict(for idempotency) or401/403(for expired nonce), it flags a replay risk. - Analyzes the OpenAPI spec for Grape endpoints that lack security scheme requirements or have no parameters for timestamps/nonces.
The scan report will highlight vulnerable endpoints under the Authentication category, often with a severity of High, and provide the specific request that was successfully replayed. This aligns with OWASP API Top 10: API2:2023 — Broken Authentication.
Grape-Specific Remediation
Remediation requires implementing a server-side, per-request uniqueness check. In Grape, this is typically done via a before filter or a custom helper that integrates with a fast store like Redis.
Pattern 1: Nonce-Based Replay Protection
Generate a nonce for each authenticated request and store it with a short TTL. Reject any request presenting a previously seen nonce.
require 'securerandom'
require 'redis'
module API
class Base < Grape::API
before :validate_nonce!
helpers do
def redis
@redis ||= Redis.new(url: ENV['REDIS_URL'])
end
def validate_nonce!
nonce = headers['X-Nonce']
error!({ error: 'Missing nonce' }, 400) unless nonce
# Use SET with NX (only set if not exists) and EX (expire in seconds)
# Returns true if key was set (new nonce), false if already exists
unless redis.set(nonce, '1', nx: true, ex: 300)
error!({ error: 'Replay detected' }, 409)
end
end
def authenticate!
# ... existing token validation logic ...
end
end
end
endPattern 2: Timestamp + Nonce (Defense in Depth)
Combine a short-lived timestamp window with a nonce to prevent both replay and clock skew attacks.
helpers do
def validate_timestamp_and_nonce!
timestamp = headers['X-Request-Timestamp'].to_i
nonce = headers['X-Nonce']
error!({ error: 'Missing timestamp or nonce' }, 400) if timestamp.zero? || nonce.nil?
# Reject requests older than 5 minutes (300 seconds)
current_time = Time.now.to_i
if (current_time - timestamp).abs > 300
error!({ error: 'Timestamp out of window' }, 400)
end
# Nonce check as above
unless redis.set("nonce:#{nonce}", '1', nx: true, ex: 300)
error!({ error: 'Replay detected' }, 409)
end
end
endPattern 3: Idempotency Key for State-Changing Operations
For POST/PUT/PATCH, require an Idempotency-Key header. Store the hash of the key + request body to detect exact replays, but also bind it to the user and operation type to allow the same key for different endpoints.
helpers do
def validate_idempotency_key!
key = headers['Idempotency-Key']
error!({ error: 'Missing Idempotency-Key' }, 400) unless key
# Create a composite key: user_id + HTTP method + path + idempotency_key
composite_key = "idem:#{current_user.id}:#{request.request_method}:#{request.path}:#{key}"
# Store the hash of the request body to ensure identical payloads
payload_hash = Digest::SHA256.hexdigest(request.body.read)
request.body.rewind # Reset stream for downstream use
stored_hash = redis.get(composite_key)
if stored_hash == payload_hash
# Return the cached successful response (or a 200 with a specific header)
# For simplicity, we return 409 if a duplicate is detected mid-processing
error!({ error: 'Duplicate request' }, 409)
else
# Set the key with a long TTL (e.g., 24 hours) after successful processing
# This is typically done after the business logic completes
# redis.setex(composite_key, 86400, payload_hash)
end
end
end
class Transactions < Grape::API
before :validate_idempotency_key!, only: [:post]
# ...
endImportant: Always perform nonce/idempotency checks after authentication but before business logic. The nonce store must be fast (Redis) and have automatic expiration to avoid memory leaks.
After implementing these fixes, rescan the API with middleBrick. The Authentication and Input Validation category scores should improve, and the replay finding should disappear from the report.