HIGH race conditiondjangohmac signatures

Race Condition in Django with Hmac Signatures

Race Condition in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability

A race condition in Django when HMAC signatures are used typically arises around signature verification timing and state-dependent checks. If your application compares signatures or tokens in a way that is not constant-time and relies on mutable shared state (for example, a cache key representing whether a signature has already been used), an attacker can exploit timing differences or force signature reuse to bypass intended guarantees.

Consider a Django view that signs a payload with HMAC and expects the signature to be used only once. A naive implementation might compute the signature, store a flag in a cache keyed by the signature or a related identifier, and then verify the signature and the flag before processing. If the check for the flag and the signature verification are not performed atomically, an attacker can send the same signed request in rapid succession. In such a scenario, the first request may clear the flag after processing, while the second request passes the initial check before the flag is cleared, resulting in the operation being performed twice.

Another common pattern involves using the signature to bind a token to a user or session. If the token is stored in the database or cache and the read–modify–write cycle is not atomic, two parallel requests can both see the token as unused and proceed. This is especially risky when the signature itself does not embed revocation state and the enforcement relies on external state that can change between verification and action.

These issues are not inherent to HMAC itself—HMAC is a secure mechanism for ensuring integrity—but to the surrounding application logic. The risk is often realized when developers focus on cryptographic correctness while overlooking atomicity, ordering, and timing in state checks. For instance, Django’s cache framework may return stale values under high concurrency if the backend does not provide strict consistency, and using such a cache for one-time checks without additional safeguards can open the door to race conditions.

In security testing, this class of flaw can be detected by observing whether the same signed payload can be accepted more than once within a short window or under concurrent load. Findings often map to OWASP API Top 10 categories related to broken object level authorization and improper concurrency control, and may intersect with risk scenarios such as BOLA/IDOR when the signature is used to authorize access to a specific resource.

Hmac Signatures-Specific Remediation in Django — concrete code fixes

To mitigate race conditions with HMAC signatures in Django, design your verification and state-changing logic to be atomic and avoid relying on non-atomic external state checks. Below are concrete patterns and code examples that reduce risk.

1. Use constant-time comparison and avoid timing leaks

Always compare signatures using a constant-time function to prevent timing attacks. Django does not provide a built-in constant-time HMAC comparison, but you can use hmac.compare_digest (Python 3.3+), which is designed for this purpose.

import hmac
import hashlib
def verify_signature(message: bytes, signature: str, secret_key: str) -> bool:
    expected = hmac.new(secret_key.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

2. Encode usage state within the signed payload

Instead of storing one-time-use flags externally, embed a usage marker (e.g., a nonce or a short-lived timestamp) inside the signed data. Verify the marker as part of the signature and reject replays based on its validity window.

import time
import hmac
import hashlib
import json
def create_signed_token(data: dict, secret_key: str, expiry_seconds: int = 300) -> str:
    payload = data.copy()
    payload['iat'] = int(time.time())
    payload['exp'] = payload['iat'] + expiry_seconds
    payload['nonce'] = os.urandom(8).hex()
    message = json.dumps(payload, sort_keys=True).encode()
    signature = hmac.new(secret_key.encode(), message, hashlib.sha256).hexdigest()
    return json.dumps({'payload': payload, 'signature': signature})

def validate_signed_token(token: str, secret_key: str) -> dict:
    parsed = json.loads(token)
    payload = parsed['payload']
    received_sig = parsed['signature']
    # Remove signature before recomputing
    message = json.dumps({k: payload[k] for k in sorted(payload) if k != 'signature'}, sort_keys=True).encode()
    expected_sig = hmac.new(secret_key.encode(), message, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected_sig, received_sig):
        raise ValueError('Invalid signature')
    if payload['exp'] < time.time():
        raise ValueError('Token expired')
    # Here you can also check a server-side seen-nonce store with atomic set-if-absent
    return payload

3. Use atomic cache operations for one-time checks

If you must track seen nonces or signatures, use cache operations that test-and-set in a single step to avoid TOCTOU windows. For example, with Django’s cache framework, prefer add (which only sets if the key does not exist) and perform the verification and storage within the same logical flow.

from django.core.cache import cache
def process_once_via_cache(signature: str) -> bool:
    # add returns True if the key was set; False if it already existed
    was_set = cache.add(f'seen_{signature}', True, timeout=300)
    return was_set  # True means first time, proceed; False means already seen, reject

Combine this with the constant-time verification from step 1. Only proceed with the business logic if both signature verification and the atomic cache check succeed.

4. Prefer short-lived signatures and server-side replay caches

Keep the validity window tight (for example, a few minutes) and store used nonces or signatures in a fast, strongly consistent store with TTL slightly longer than the signature lifetime. This reduces the window in which a race can be exploited and ensures that old signatures cannot be reused indefinitely.

These practices address the race condition risks by removing external, mutable state checks that are not atomic and by ensuring that replay detection is performed in a way that cannot be bypassed by concurrent requests. They remain compatible with the ecosystem tools you already use and do not require changes to the core HMAC algorithm.

Frequently Asked Questions

Can a race condition with HMAC signatures be detected by black-box scanning?
Yes, a black-box scan can surface indicators by submitting the same signed request concurrently or in rapid succession and observing whether the operation is accepted more than once. Findings are reported with severity and remediation guidance.
How does using nonces inside the signed payload reduce replay risk?
Embedding a nonce or timestamp inside the signed payload binds replay detection to signature verification. The server validates the signature first, then checks the nonce’s freshness and uniqueness using an atomic check, making it significantly harder to reuse a token without detection.