HIGH cross site request forgeryflaskhmac signatures

Cross Site Request Forgery in Flask with Hmac Signatures

Cross Site Request Forgery in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Cross Site Request Forgery (CSRF) in Flask when HMAC signatures are used incorrectly can still lead to authorization requests being forged if the signature does not adequately bind the request context. A common pattern is to sign a subset of request properties (such as the endpoint path, selected headers, or a timestamp) and verify the signature on the server. If the signature scope is too narrow or if the server relies only on the signature without validating the origin or referer, an attacker can trick a victim’s browser into sending a request whose signed parameters are valid, but whose intent the victim did not authorize.

Consider a Flask endpoint that accepts money transfers using a query parameter signature. The client computes an HMAC over the concatenated path and a transaction amount, and the server recomputes and compares the HMAC. An attacker can construct a request to the same endpoint with a different amount if the signature does not include all necessary context or if the server fails to bind the request to the user’s session. Because the signature is valid, the server may treat the request as legitimate. In a browser context, an attacker can embed an image or form submission pointing to the vulnerable endpoint, and if the victim is authenticated, the signed request may be sent automatically with cookies, leading to unauthorized actions.

With OpenAPI/Swagger spec analysis, tools like middleBrick can detect whether endpoints that accept HMAC-signed requests also validate the Origin and Referer headers or enforce anti-CSRF tokens in forms. If the spec defines security schemes based solely on signatures and does not mention CSRF mitigation for state-changing methods, runtime checks may flag the unauthenticated attack surface where a forged request could be crafted without user interaction. Even when HMACs protect integrity, they do not inherently provide request authenticity from the browser’s perspective; without additional context binding, the signature alone cannot prevent CSRF.

For example, a signature that covers only the URL path and body but ignores the request method or does not enforce SameSite cookies leaves room for exploitation. An attacker can lure a user to a malicious site that issues a POST to the signed endpoint using a form, leveraging the user’s existing authentication cookies. Because the signature matches the crafted parameters, the server processes the request as intended by the attacker. This illustrates why HMAC signatures must be part of a broader defense that includes CSRF tokens or strict SameSite and anti-CSRF header validation for state-changing operations in Flask.

Hmac Signatures-Specific Remediation in Flask — concrete code fixes

To mitigate CSRF when using HMAC signatures in Flask, ensure the signature scope covers all request dimensions that must be immutable and binding, and couple verification with anti-CSRF controls for browser-originated requests. Below are concrete code examples that show a safer pattern: signing the method, path, selected headers, a nonce, and a timestamp, and verifying both the signature and a SameSite cookie plus a CSRF token for state-changing methods.

Server-side signature verification with CSRF token and SameSite checks

import hashlib
import hmac
import time
from flask import Flask, request, make_response, abort, get_flashed_messages

app = Flask(__name__)
SECRET_KEY = b'your-secure-secret-key'  # store securely, e.g., from env
NONCE_STORE = set()  # in production use a short-lived cache like Redis

def compute_hmac(method, path, headers_subset, nonce, timestamp, body_bytes):
    payload = f'{method}\n{path}\n{headers_subset}\n{nonce}\n{timestamp}\n{body_bytes.hex()}'
    return hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).hexdigest()

@app.before_request
def verify_hmac_and_csrf():
    if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
        # Expect a CSRF token in header for state-changing requests
        csrf_token = request.headers.get('X-CSRF-Token')
        if not csrf_token or csrf_token != session.get('csrf_token'):
            abort(403, description='Missing or invalid CSRF token')

        # Ensure SameSite cookie is present and validated (example: session cookie)
        if not request.cookies.get('session'):
            abort(401, description='Session cookie missing')

    if request.method == 'GET':
        # For safe methods, we may only validate signature for sensitive GETs if needed
        return

    # Reconstruct what the client signed
    method = request.method
    path = request.path
    # include a subset of headers that are relevant for authorization
    headers_subset = request.headers.get('X-Request-ID', '')
    timestamp = request.headers.get('X-Timestamp')
    nonce = request.headers.get('X-Nonce')
    body_bytes = request.get_data()

    if not all([timestamp, nonce]):
        abort(400, description='Missing timestamp or nonce')

    # Basic replay / freshness protection
    if int(timestamp) < time.time() - 300:
        abort(400, description='Request too old')
    if nonce in NONCE_STORE:
        abort(400, description='Replay detected')
    NONCE_STORE.add(nonce)

    received_sig = request.headers.get('X-Signature')
    expected_sig = compute_hmac(method, path, headers_subset, nonce, timestamp, body_bytes)
    if not hmac.compare_digest(expected_sig, received_sig):
        abort(403, description='Invalid signature')

@app.route('/transfer', methods=['POST'])
def transfer():
    # Business logic here; signature and CSRF have already been verified
    return {'status': 'ok'}

In this pattern, the HMAC covers method, path, a selected header, a nonce, a timestamp, and the request body hex. Verification includes checking a SameSite session cookie, a per-request CSRF token in a header, timestamp freshness, and nonce replay protection. This binds the signature to the request context and prevents CSRF because an attacker cannot provide a valid CSRF token or a valid nonce/timestamp combination from another origin.

Generating the signature on the client (example in Python)

import hashlib
import hmac
import time
import requests

SECRET_KEY = b'your-secure-secret-key'

def compute_hmac(method, path, headers_subset, nonce, timestamp, body_bytes):
    payload = f'{method}\n{path}\n{headers_subset}\n{nonce}\n{timestamp}\n{body_bytes.hex()}'
    return hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).hexdigest()

nonce = 'unique-nonce-per-request'
timestamp = str(int(time.time()))
headers_subset = 'my-request-id-123'
body = b'{"account":"A","amount":100}'
sig = compute_hmac('POST', '/transfer', headers_subset, nonce, timestamp, body)

headers = {
    'X-Request-ID': headers_subset,
    'X-Timestamp': timestamp,
    'X-Nonce': nonce,
    'X-Signature': sig,
    'X-CSRF-Token': 'session-bound-csrf-token',  # obtained from server or cookie
    'Content-Type': 'application/octet-stream'
}
resp = requests.post('https://api.example.com/transfer', data=body, headers=headers)
print(resp.status_code, resp.json())

By combining HMAC verification with CSRF tokens and SameSite cookie practices, Flask applications can defend against CSRF even when signatures are used for integrity. middleBrick can complement this by scanning the OpenAPI spec and runtime behavior to verify that endpoints requiring authentication also include checks for CSRF-relevant headers and that unauthenticated attack surfaces are minimized.

Frequently Asked Questions

Does a valid HMAC alone prevent CSRF in Flask?
No. A valid HMAC ensures integrity and authenticity of the signed parameters, but it does not bind the request to the user's browser context. Without additional CSRF protections such as SameSite cookies, anti-CSRF tokens, or origin/referer checks, an attacker can still forge requests using the victim’s cookies.
What should be included in the HMAC scope to reduce CSRF risk?
Include the HTTP method, request path, a relevant subset of headers (e.g., an idempotency key or request ID), a nonce, a timestamp, and the request body. This binds the signature to the full request context and makes it difficult for an attacker to craft a valid forged request.