Replay Attack in Flask
How Replay Attack Manifests in Flask
A replay attack in a Flask API occurs when an attacker intercepts a legitimate, authenticated request and resubmits it to perform an unauthorized action. Because Flask applications are often stateless and rely on tokens (like JWTs) for authentication, they can be vulnerable if the token or request itself lacks protections against duplication. The core issue is that the server cannot distinguish between the original request and a replayed one.
In Flask, this commonly appears in endpoints that handle state-changing operations (e.g., financial transactions, password changes, or order placements) where the authentication mechanism validates the token's signature and claims but does not verify the request's uniqueness or freshness. A typical vulnerable pattern uses Flask-JWT-Extended or a similar library to protect a route but omits any nonce (number used once) or timestamp validation.
Flask-Specific Attack Pattern: Consider a Flask route that processes a fund transfer using a JWT for user identification. The JWT may have an expiration (exp), but without additional checks, an attacker who captures the HTTP request (e.g., via a man-in-the-middle attack on non-HTTPS traffic or a compromised client) can replay it multiple times. Each replay will be accepted as a new, valid request because the token is still within its expiration window and the server has no memory of previous requests.
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'super-secret'
jwt = JWTManager(app)
@app.route('/transfer', methods=['POST'])
@jwt_required()
def transfer():
data = request.get_json()
user_id = get_jwt_identity()
amount = data['amount']
# Vulnerable: No check for request replay
# Process the transfer...
return jsonify(msg=f'Transferred {amount} from {user_id}'), 200
# Attacker captures this request and replays it
# POST /transfer with valid JWT and body {"amount": 100}
Here, each replayed request executes the transfer logic again. The vulnerability is exacerbated if the JWT has a long expiration or if the endpoint does not require additional transaction-specific verification (like a one-time token). This is a classic Broken Authentication flaw (OWASP API Top 10 A02:2021) that can lead to financial loss, data duplication, or privilege escalation if combined with BOLA/IDOR issues.
Flask-Specific Detection
Detecting replay vulnerabilities in Flask APIs requires examining both the codebase and the runtime behavior. Since middleBrick performs black-box scanning without credentials or configuration, it dynamically tests for this issue by sending identical requests multiple times and analyzing the responses and side effects.
Manual Code Review Indicators:
- Absence of nonce generation and validation in state-changing endpoints (POST/PUT/DELETE).
- Reliance solely on JWT
expclaim for request freshness without a short expiration window (e.g., >5 minutes). - No use of idempotency keys (e.g.,
Idempotency-Keyheader) or timestamp/nonce parameters in the request body or headers. - Use of predictable values (like timestamps without randomness) as nonces, which could allow an attacker to guess and reuse them.
Dynamic Testing with middleBrick: During a scan, middleBrick's Authentication and BOLA/IDOR checks will automatically probe for replay vulnerabilities. It works by:
- Identifying an endpoint that requires authentication (e.g., returns 401 without a token).
- Obtaining a valid token (if the API has a login endpoint) or using a provided token.
- Sending a state-changing request (e.g., a POST that creates a resource) and recording the response and any side effects (like a new database entry).
- Re-sending the exact same request (including headers and body) multiple times.
- Checking if subsequent requests succeed (e.g., return 200) and produce new side effects (e.g., duplicate resources). If so, the endpoint is vulnerable to replay.
This is a form of active testing, similar to OWASP's Testing for Replay Attacks (WSTG-SESSION-07). middleBrick's report will flag this under the Authentication category with a severity based on the endpoint's sensitivity (e.g., critical for financial transactions). The findings include the specific request that was replayed and the evidence of duplication.
Symptom vs. Detection Method:
| Symptom in Flask API | How middleBrick Detects It |
|---|---|
| Duplicate resources created from same request | Replays request, compares response bodies and status codes; looks for new resource IDs in responses. |
| Same action executed multiple times (e.g., multiple charges) | Analyzes side effects by re-running the request and checking for repeated state changes (e.g., balance decrement). |
| No error on repeated nonce or timestamp | If a nonce is present but not validated server-side, replay will still succeed. |
Flask-Specific Remediation
Remediating replay attacks in Flask requires implementing server-side request uniqueness checks. Since Flask is stateless, you must use a fast, shared storage (like Redis) to track nonces or processed request IDs across application instances. middleBrick's findings provide prioritized remediation guidance, which for replay attacks typically includes:
1. Use Short-Lived JWTs with a Robust exp Claim
Set the JWT expiration to a short duration (e.g., 5–15 minutes) to limit the replay window. However, this alone is insufficient for high-security operations because an attacker can still replay within that window.
# Create token with 10-minute expiration
access_token = create_access_token(identity=user.id, expires_delta=timedelta(minutes=10))2. Implement a Nonce Mechanism with Redis
Generate a cryptographically random nonce for each sensitive request. Store a hash of the nonce in Redis with a short TTL (matching the JWT expiration). On request receipt, check if the nonce has been seen before; if yes, reject the request.
import secrets
import redis
from flask import request, jsonify
redis_client = redis.Redis(host='localhost', port=6379, db=0)
@app.route('/transfer', methods=['POST'])
@jwt_required()
def transfer():
nonce = request.headers.get('X-Request-Nonce')
if not nonce:
return jsonify(error='Missing nonce'), 400
# Check if nonce already used
if redis_client.get(f'nonce:{nonce}'):
return jsonify(error='Replay detected'), 409
# Store nonce with TTL (e.g., 10 minutes)
redis_client.setex(f'nonce:{nonce}', 600, '1')
# Process transfer...
return jsonify(msg='Transfer successful'), 200
# Client must generate a new random nonce for each request
# curl -H "Authorization: Bearer " -H "X-Request-Nonce: $(openssl rand -hex 16)" ...
3. Enforce Idempotency Keys for Critical Operations
For operations like payments, require an Idempotency-Key header (a UUID generated by the client). Store the key and its result in Redis. If the same key is received again, return the previous response without re-executing.
import uuid
@app.route('/payment', methods=['POST'])
@jwt_required()
def payment():
idempotency_key = request.headers.get('Idempotency-Key')
if not idempotency_key:
return jsonify(error='Missing Idempotency-Key'), 400
# Check if we've processed this key
cached = redis_client.get(f'idempotency:{idempotency_key}')
if cached:
return jsonify(json.loads(cached)), 200 # Return cached response
# Process payment...
result = {'status': 'paid', 'transaction_id': str(uuid.uuid4())}
redis_client.setex(f'idempotency:{idempotency_key}', 86400, json.dumps(result)) # Cache for 24h
return jsonify(result), 200
4. Use HTTPS Everywhere While not a Flask-specific fix, always serve APIs over TLS to prevent network eavesdropping. Flask-Talisman can help enforce HTTPS.
middleBrick's remediation guidance maps these fixes to OWASP API Top 10 (A02:2021) and compliance frameworks like PCI-DSS (requirement 6.5.1 for improper access control) and SOC2 (CC6.1). The Pro plan's continuous monitoring can verify that these protections remain in place after deployment.