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-LengthandTransfer-Encodingin 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.