HIGH rate limiting bypassfastapihmac signatures

Rate Limiting Bypass in Fastapi with Hmac Signatures

Rate Limiting Bypass in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Rate limiting in FastAPI often relies on identifying clients by a static value such as an API key in a header. When HMAC signatures are used to authorize each request, a server may treat the signature as an opaque, authenticated identifier and map the rate limit to the key ID extracted from the signature payload. This creates a vulnerability because an attacker who can generate valid signatures under a low-rate key may be able to force each request to appear as if it comes from different authorized keys, bypassing aggregate limits.

Specifically, if the rate limiter is scoped only to the key ID and does not also consider the signer’s identity or a per-client nonce, an attacker can rotate signing keys (or use multiple compromised keys) while keeping each individual request under the threshold. Because HMAC ensures integrity but does not by itself enforce policy, the application may fail to detect that the same attacker is responsible for a high volume of requests. This becomes more likely when the signature includes mutable or non-unique claims such as timestamps with small windows or nonces that are not enforced for uniqueness across requests.

Another bypass pattern arises when the server validates the HMAC but does not enforce rate limiting on the validation endpoint itself, allowing an attacker to flood the verification path with crafted requests. If the rate limiter is implemented at a higher layer (for example, a gateway) but not consistently applied to all FastAPI routes, an attacker can target unguarded endpoints that still perform business logic. Poorly designed replay protection, such as accepting a broad time window for nonce values, can also enable replay attacks where captured signed requests are reissued to exhaust per-client quotas.

In practice, a realistic scenario involves a FastAPI service that signs requests with a shared secret and includes a client identifier and a short-lived timestamp in the signed payload. If the server applies rate limits only on the client identifier and does not track the number of distinct timestamps or signatures per client, an attacker with access to multiple valid keys can cycle through them while remaining under each key’s limit. Alternatively, if timestamps are not strictly validated for monotonicity or freshness, an attacker may reuse recent signed payloads within the allowed window, effectively diluting the rate limit across repeated replays.

These bypasses highlight that HMAC signatures provide authenticity and integrity, but they do not automatically enforce authorization or policy constraints such as rate limits. The application must explicitly correlate the signer, the nonce or timestamp, and the rate bucket in a consistent manner across all endpoints. Without binding the rate limit to a combination of client identity, signature context, and anti-replay controls, attackers can exploit gaps between authentication and enforcement to exceed intended request volumes.

Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes

To mitigate rate limiting bypass with HMAC signatures in FastAPI, bind rate limits to both the key identifier and the signature context, including nonce or timestamp, and enforce strict validation. Below is a minimal but realistic example that signs a request with HMAC-SHA256, includes a timestamp and a nonce, and verifies both signature integrity and freshness before applying rate limits.

import time
import hmac
import hashlib
import base64
from fastapi import FastAPI, Request, HTTPException, Header, Depends
from typing import Dict

app = FastAPI()

# Example shared secret store (use a secure vault in production)
SHARED_SECRETS: Dict[str, bytes] = {
    "client_a": b"super-secret-key-a",
    "client_b": b"super-secret-key-b",
}

def verify_hmac_signature(
    method: str,
    path: str,
    timestamp: str,
    nonce: str,
    signature: str,
    client_id: str,
) -> bool:
    if client_id not in SHARED_SECRETS:
        return False
    secret = SHARED_SECRETS[client_id]
    message = f"{method}\n{path}\n{timestamp}\n{nonce}\n".encode()
    expected = base64.b64encode(
        hmac.new(secret, message, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature)

def rate_limit_key(request: Request) -> str:
    # Use client ID, timestamp bucket, and optional nonce window to scope limits
    client_id = request.headers.get("X-Client-ID")
    timestamp = request.headers.get("X-Timestamp")
    nonce = request.headers.get("X-Nonce")
    if not client_id or not timestamp or not nonce:
        raise HTTPException(status_code=400, detail="Missing required headers")
    # A simple per-minute bucket; adjust granularity as needed
    bucket = str(int(timestamp) // 60)
    return f"{client_id}:{bucket}:{nonce[:8]}"

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    # Only enforce on endpoints that require auth
    if request.url.path.startswith("/api/"):
        client_id = request.headers.get("X-Client-ID")
        timestamp = request.headers.get("X-Timestamp")
        nonce = request.headers.get("X-Nonce")
        signature = request.headers.get("X-Signature")
        if not all([client_id, timestamp, nonce, signature]):
            raise HTTPException(status_code=400, detail="Missing authentication headers")
        if not verify_hmac_signature(
            request.method,
            request.url.path,
            timestamp,
            nonce,
            signature,
            client_id,
        ):
            raise HTTPException(status_code=401, detail="Invalid signature")
        # Ensure timestamp is recent (e.g., within 5 minutes) to prevent replay
        if abs(time.time() - int(timestamp)) > 300:
            raise HTTPException(status_code=400, detail="Stale timestamp")
        # Apply rate limiting based on a composite key
        key = rate_limit_key(request)
        # Implement your rate limiter here, e.g., using Redis with key and TTL
        # Example pseudo-check:
        # if redis.get(key) >= LIMIT:
        #     raise HTTPException(status_code=429, detail="Rate limit exceeded")
        # redis.incr(key)
        # redis.expire(key, 60)
    response = await call_next(request)
    return response

In this example, the rate limiting key includes the client ID, a timestamp bucket (e.g., per minute), and a portion of the nonce. This ensures that even if one client key is compromised, the rate limit is scoped tightly to prevent an attacker from reusing buckets or nonces to inflate volume. Always validate timestamps to mitigate replay attacks and ensure nonces are unique and short-lived.

Additionally, enforce rate limiting on the signature verification path itself and avoid exempting endpoints merely because they perform validation. Use a consistent backend store (such as Redis) to track counts atomically and apply TTLs aligned with your bucket size. Combining these measures binds the limit to the authenticated context defined by the HMAC signature, reducing the risk of bypass through key rotation or replay.

Related CWEs: resourceConsumption

CWE IDNameSeverity
CWE-400Uncontrolled Resource Consumption HIGH
CWE-770Allocation of Resources Without Limits MEDIUM
CWE-799Improper Control of Interaction Frequency MEDIUM
CWE-835Infinite Loop HIGH
CWE-1050Excessive Platform Resource Consumption MEDIUM

Frequently Asked Questions

How does HMAC fail to prevent rate limiting bypass if it ensures integrity?
HMAC guarantees that a request has not been altered, but it does not enforce policy. If the server maps rate limits only to a key ID within the HMAC payload and does not also consider nonce/timestamp uniqueness or bind limits to the full signed context, an attacker can rotate keys or replay requests within the allowed window to exceed aggregate limits.
What specific header design helps prevent rate limiting bypass with HMAC signatures?
Include X-Client-ID, X-Timestamp, X-Nonce, and X-Signature on every request. Ensure the server validates the timestamp freshness, enforces nonce uniqueness, and scopes the rate limit key to client ID plus timestamp bucket plus a truncated nonce, rather than trusting only the client ID.