HIGH request smugglingflaskhmac signatures

Request Smuggling in Flask with Hmac Signatures

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

Request smuggling arises when a backend service like Flask processes requests differently than an upstream proxy or load balancer, allowing an attacker to smuggle requests across trust boundaries. When HMAC signatures are used for request authentication but the application does not enforce strict header parsing and message boundary validation, the combination can expose or amplify smuggling risks.

In Flask, developers commonly use HMAC to verify request integrity by signing a subset of headers and the body. A typical pattern is to have the client compute an HMAC over selected headers (for example, Content-Type, X-Request-ID) and the payload, then send the signature in a custom header such as X-API-Signature. If Flask’s route handler reads the body or headers in a non-canonical way—such as relying on request.get_data() after middleware has already consumed part of the stream—and the proxy parses the request message differently, an attacker can craft a request that is valid to the proxy but interpreted differently by Flask.

A concrete scenario: an HTTP proxy expects Content-Length to delimit the message body, while Flask (or an intermediary WSGI wrapper) processes Transfer-Encoding: chunked when present. If the HMAC is computed only over selected headers and the body but the proxy adds or removes headers before forwarding, the signature may still validate on the backend while the proxy interprets the message boundary differently. This mismatch allows an attacker to smuggle an additional request within the same TCP connection, potentially bypassing intended isolation between operations. For example, a request intended as a read-only query can be smuggled into a second request that modifies state, because Flask processes the second request with the same authenticated context.

Flask does not provide built-in canonicalization for HMAC-signed requests. If developers combine request.get_json() or request.form with manual signature verification after the fact, they risk inconsistent parsing across the proxy and the application. The vulnerability is not in HMAC itself, but in how the framework and surrounding infrastructure interpret message boundaries and which headers are included in the signed payload. Without a strict, shared parsing policy between proxy and app, an attacker can exploit differences to bypass intended access controls or authentication checks, leading to unauthorized actions or information exposure.

Hmac Signatures-Specific Remediation in Flask — concrete code fixes

To mitigate request smuggling when using HMAC signatures in Flask, enforce a single, canonical parsing strategy and ensure the signature covers all headers that influence message boundary interpretation. Use a deterministic header order and include critical headers such as Content-Length or the presence of Transfer-Encoding in the signed payload. Avoid relying on framework-provided request accessors before verifying the signature, and validate that the parsed message boundaries match the proxy’s expectations.

Below are concrete, secure code examples for Flask that demonstrate HMAC verification with canonical header handling and body integrity checks.

Example 1: Canonical HMAC verification with explicit body and headers

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

app = Flask(__name__)
SECRET_KEY = b'super-secret-key'  # store securely, e.g., from env

def compute_signature(headers, body):
    # Canonical header string: sort and join key:value with newline
    header_lines = [f'{k.lower()}:{v.strip()}' for k, v in sorted(headers.items())]
    message = '\n'.join(header_lines) + '\n' + body
    return hmac.new(SECRET_KEY, message.encode('utf-8'), hashlib.sha256).hexdigest()

@app.before_request
def verify_hmac():
    # Read raw body once and preserve it for downstream use
    raw_body = request.get_data(as_text=True)
    received_sig = request.headers.get('X-API-Signature')
    if not received_sig:
        return jsonify(error='missing signature'), 401
    # Include only the headers you intend to trust in the signature
    signed_headers = {
        'content-type': request.headers.get('Content-Type', ''),
        'x-request-id': request.headers.get('X-Request-ID', ''),
        'transfer-encoding': request.headers.get('Transfer-Encoding', ''),
        'content-length': request.headers.get('Content-Length', ''),
    }
    expected_sig = compute_signature(signed_headers, raw_body)
    if not hmac.compare_digest(expected_sig, received_sig):
        return jsonify(error='invalid signature'), 403
    # Optionally, ensure message boundary consistency
    if 'Content-Length' in request.headers and 'Transfer-Encoding' in request.headers:
        return jsonify(error='conflicting transfer encodings'), 400
    # Attach verified body for downstream routes if needed
    request._verified_body = raw_body

@app.route('/api/action', methods=['POST'])
def api_action():
    # Use request.get_data() to avoid re-parsing after before_request modifications
    data = request.get_data(as_text=True)
    return jsonify(status='ok', received_len=len(data))

if __name__ == '__main__':
    app.run(debug=False)

Example 2: Reject ambiguous message encodings and enforce strict parsing

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

app = Flask(__name__)
SECRET_KEY = b'another-secret'

def verify_request():
    # Reject requests that mix Transfer-Encoding and Content-Length to prevent smuggling
    te = request.headers.get('Transfer-Encoding', '')
    cl = request.headers.get('Content-Length', '')
    if te and cl:
        return jsonify(error='ambiguous message encoding'), 400
    body = request.get_data(as_text=True)
    sig = request.headers.get('X-API-Signature')
    headers_for_sig = {
        'method': request.method,
        'content-type': request.headers.get('Content-Type', ''),
        'content-length': request.headers.get('Content-Length', ''),
        'transfer-encoding': te,
    }
    parts = [f'{k.lower()}:{v.strip()}' for k, v in sorted(headers_for_sig.items())]
    message = '\n'.join(parts) + '\n' + body
    expected = hmac.new(SECRET_KEY, message.encode('utf-8'), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return jsonify(error='invalid signature'), 403
    return None

@app.before_request
def before():
    err = verify_request()
    if err:
        # return err directly to stop request processing
        pass  # placeholder for early abort; in practice return err from before_request

@app.route('/checkout', methods=['POST'])
def checkout():
    return jsonify(processed=True)

Key remediation practices:

  • Include Content-Length and Transfer-Encoding in the HMAC when relevant, and reject requests where both are present.
  • Use a single, deterministic method to build the string that is signed, with a consistent header order and lowercasing policy.
  • Read the raw body once with request.get_data() before any parsing that might change the stream, and reuse that bytes/string for verification and downstream logic.
  • Ensure your proxy and application share the same interpretation of message boundaries; do not rely on framework defaults to reconcile differences.

Frequently Asked Questions

Why does including Transfer-Encoding and Content-Length in the HMAC help prevent request smuggling?
Including both headers in the signature ensures that any mismatch in how the proxy and Flask interpret message boundaries will break the HMAC verification. An attacker cannot change one without invalidating the signature, preventing smuggling attempts that rely on header manipulation.
Can request smuggling occur even when HMAC signatures are verified correctly?
Yes, if the proxy and Flask parse the request differently (for example, one treats a header as terminating the body while the other does not), an attacker can smuggle requests even with valid signatures. Canonicalizing which headers influence boundaries and enforcing consistent parsing across layers is essential.