HIGH replay attackflaskapi keys

Replay Attack in Flask with Api Keys

Replay Attack in Flask with Api Keys — how this specific combination creates or exposes the vulnerability

A replay attack occurs when an attacker intercepts a valid request—including an API key—and re-sends it to the API at a later time to gain unauthorized access or cause unintended effects. In Flask applications that rely solely on static API keys for authentication, this risk is pronounced because the key itself is long-lived and does not inherently bind to a specific context beyond the initial client–server exchange.

Consider a Flask endpoint that expects an API key in a request header:

from flask import Flask, request, jsonify

app = Flask(__name__)
VALID_API_KEYS = {"sk_live_abc123", "sk_test_xyz789"}

@app.route("/transfer", methods=["POST"])
def transfer():
    api_key = request.headers.get("X-API-Key")
    if api_key not in VALID_API_KEYS:
        return jsonify({"error": "unauthorized"}), 401
    # process transfer
    return jsonify({"status": "ok"})

If this endpoint lacks additional protections—such as a nonce, timestamp, or request signature—an attacker who captures a valid HTTPS request (e.g., via a compromised network or insecure logging) can simply replay the exact same request later. The API key remains valid, and Flask will treat the replayed request as legitimate. This is a BOLA/IDOR-style problem in the context of authentication: the identifier (API key) is present and accepted, but the request’s context (time, intent, uniqueness) is not verified.

In a black-box scan, middleBrick tests this scenario by submitting the same request twice with identical headers and body. If the second response is identical to the first (or produces the same side effect), a finding is raised under BOLA/IDOR and Unsafe Consumption categories. The scanner also checks whether API keys are transmitted over non-encrypted channels and whether they appear in logs or error messages, which would increase Data Exposure risk.

Replay attacks against API-key-protected Flask services are especially concerning when the operations are non-idempotent—such as financial transfers or configuration changes. The presence of an API key alone does not guarantee that the request is fresh or intended by the legitimate owner. Complementary risks include insufficient Rate Limiting, which allows an attacker to resend captured requests at scale, and missing Input Validation, which may allow slight modifications of the replayed payload to bypass secondary checks.

Because API keys are often stored as static secrets, they do not rotate frequently. If a key is ever exposed, an attacker can replay historic requests until the key is rotated. middleBrick’s LLM/AI Security checks do not directly test replay, but they highlight broader weaknesses in how endpoints handle unauthenticated or improperly authenticated traffic, which can coincide with weak key management practices.

Api Keys-Specific Remediation in Flask — concrete code fixes

To mitigate replay attacks while continuing to use API keys in Flask, you must introduce request-level context that prevents reuse. This involves adding nonces or one-time tokens, timestamps with short validity windows, and per-request signatures. Below are concrete, working code examples showing how to implement these patterns.

1. Nonce-Based Protection

Require clients to include a unique nonce (e.g., a UUID) and store recently seen nonces for a short window. This ensures that even if an API key is captured, the same request cannot be replayed.

import uuid
import time
from flask import Flask, request, jsonify, g

app = Flask(__name__)
VALID_API_KEYS = {"sk_live_abc123", "sk_test_xyz789"}
seen_nonces = set()
NONCE_TTL_SECONDS = 300  # 5 minutes

def prune_old_nonces():
    # In production, use a time-based cache or Redis; this is simplified.
    pass

@app.before_request
def validate_nonce():
    if request.endpoint in ["health_check"]:
        return
    api_key = request.headers.get("X-API-Key")
    nonce = request.headers.get("X-Nonce")
    if api_key not in VALID_API_KEYS:
        return jsonify({"error": "unauthorized"}), 401
    if not nonce:
        return jsonify({"error": "missing nonce"}), 400
    if nonce in seen_nonces:
        return jsonify({"error": "replay detected"}), 403
    seen_nonces.add(nonce)
    prune_old_nonces()  # implement TTL cleanup

@app.route("/transfer", methods=["POST"])
def transfer():
    # process transfer
    return jsonify({"status": "ok"})

2. Timestamp + Skew Window

Include a timestamp in the request and reject requests with timestamps too far from the server time. This limits the window during which a captured request can be reused.

from flask import Flask, request, jsonify
import time

app = Flask(__name__)
VALID_API_KEYS = {"sk_live_abc123", "sk_test_xyz789"}
SKEW_SECONDS = 60  # allow 1-minute clock skew

@app.route("/action", methods=["POST"])
def action():
    api_key = request.headers.get("X-API-Key")
    timestamp = request.headers.get("X-Timestamp")
    if api_key not in VALID_API_KEYS:
        return jsonify({"error": "unauthorized"}), 401
    if not timestamp:
        return jsonify({"error": "missing timestamp"}), 400
    try:
        req_time = float(timestamp)
    except ValueError:
        return jsonify({"error": "invalid timestamp format"}), 400
    if abs(time.time() - req_time) > SKEW_SECONDS:
        return jsonify({"error": "stale request"}), 403
    # process action
    return jsonify({"status": "ok"})

3. Signature-Based Validation (HMAC)

Have clients sign the request payload and selected headers using a shared secret. The server recomputes the signature and rejects mismatches. This binds the entire request context and prevents tampering or replay.

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
SHARED_SECRETS = {"client1": "supersecretkey123"}

def verify_signature(api_key, body, signature):
    secret = SHARED_SECRETS.get(api_key)
    if not secret:
        return False
    computed = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, signature)

@app.route("/order", methods=["POST"])
def order():
    api_key = request.headers.get("X-API-Key")
    signature = request.headers.get("X-Signature")
    if not verify_signature(api_key, request.get_data(as_text=True), signature):
        return jsonify({"error": "invalid signature"}), 403
    # process order
    return jsonify({"status": "ok"})

These approaches ensure that API-key-based authentication in Flask is resilient to replay attacks. When combined with middleBrick scans, you can validate that such protections are correctly enforced across your unauthenticated attack surface.

Frequently Asked Questions

Can API keys alone be considered secure against replay attacks?
No. API keys alone do not prevent replay attacks because they are static and do not bind requests to a unique context. Additional protections such as nonces, timestamps, or request signatures are required.
How does middleBrick assess replay attack risks in Flask APIs using API keys?
middleBrick tests by submitting identical requests twice and checking whether the second produces the same outcome. Findings are reported under BOLA/IDOR and Unsafe Consumption categories, with remediation guidance for introducing nonces or timestamps.