Http Request Smuggling in Flask with Hmac Signatures
Http Request Smuggling in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
HTTP request smuggling arises when a backend service processes the same request differently than a frontend proxy or load balancer, often due to inconsistent parsing of ambiguous or malformed messages. In Flask applications that use HMAC signatures to validate request authenticity, the interplay between signature verification and proxy parsing can create a window for smuggling. The vulnerability is not caused by HMAC itself, but by where and how signature validation occurs in the request lifecycle and how the ingress layer interprets message boundaries.
Consider a setup where a frontend proxy (or API gateway) terminates TLS and forwards requests to a Flask app. If the proxy normalizes or parses the request body before forwarding, but Flask receives the raw, possibly malformed stream, the two may split the request differently. For example, a smuggler can craft a request that contains two Content-Length headers or an ambiguous Transfer-Encoding value. The proxy might interpret the request as one logical request and strip or modify the smuggling vector, while Flask parses the next request as part of the body and routes it to an endpoint that trusts the HMAC signature. Because the HMAC is computed over the raw payload bytes, a valid signature for one message does not guarantee the parsed route and body are aligned with the proxy’s interpretation.
In Flask, if signature validation is performed after routing (for example, inside a view or a before_request handler that runs after routing has already selected an endpoint), an attacker can exploit parsing divergence to route a request to an unintended handler. A typical pattern is to send a request like:
POST /transfer HTTP/1.1
Content-Type: application/json
Content-Length: 13
X-Signature:
{"amount": 0,
"account": "A"
"account": "B"}
The duplicate or conflicting headers can cause the proxy to treat the second “account” line as the start of a new request, while Flask may include the entire body in the first request. If the HMAC is verified only on the body bytes Flask sees, the application might process the second “account” as part of a new, unvalidated context, leading to unauthorized operations. This is especially risky when combined with endpoint-specific logic that assumes the request body is authoritative because the HMAC matched, without re-evaluating routing assumptions introduced by the proxy.
Another variant involves chunked transfer encoding. A frontend might interpret chunked bodies and forward a normalized Content-Length–based version to Flask. If the HMAC is computed on the chunked bytes, the frontend’s normalized version will have a different signature, but if the frontend strips the chunked encoding and forwards a canonical body, the HMAC may still validate on the backend if the backend also normalizes the body in the same way. An inconsistency in normalization between proxy and app can allow a smuggler to inject a request that appears authenticated to Flask but is interpreted differently upstream, bypassing intended routing or authorization checks.
Because middleBrick tests unauthenticated attack surfaces, it can surface these routing and parsing discrepancies by analyzing how the API behaves under malformed or ambiguous requests, including those with conflicting headers and encoding choices. Findings highlight whether signature validation is tightly coupled to routing and whether the app’s parsing assumptions align with the proxy layer.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring consistent parsing and strict signature placement so that routing and body interpretation are aligned between the proxy and Flask. The HMAC must be computed over the exact bytes that determine routing and authorization, and validation should occur before any routing or business logic is applied. Below are concrete, safe patterns for Flask applications.
1. Compute and verify HMAC on the raw request body before routing
Access the raw data once via request.get_data() to ensure consistent byte representation, compute the HMAC, and validate it before any route or before_request logic that depends on the body. This prevents discrepancies between what the proxy normalizes and what Flask parses.
import hmac
import hashlib
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
SECRET_KEY = b'super-secret-key' # store securely, e.g. from env
def verify_hmac():
raw = request.get_data()
signature = request.headers.get('X-Signature')
if signature is None:
abort(401, 'Missing signature')
expected = hmac.new(SECRET_KEY, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
@app.before_request
def authenticate():
# Skip verification for public endpoints if needed
if request.path == '/public/health':
return
verify_hmac()
@app.route('/transfer', methods=['POST'])
def transfer():
body = request.get_json(force=True) # safe after HMAC verified
# process transfer
return jsonify(status='ok')
2. Normalize body before HMAC if proxy and app must differ
If your architecture requires the proxy to normalize the body (e.g., strip chunked encoding or remove certain headers), ensure Flask performs the same normalization before computing or verifying the HMAC. Avoid double-normalization or partial normalization that creates divergence.
import json
def normalize_json_body(raw: bytes) -> bytes:
try:
data = json.loads(raw)
# canonicalize JSON: sorted keys, no extra whitespace
return json.dumps(data, separators=(',', ':'), sort_keys=True).encode('utf-8')
except json.JSONDecodeError:
# If not JSON, fall back to raw; ensure proxy and app agree on fallback
return raw
@app.before_request
def authenticate_with_normalized():
if request.path == '/public/health':
return
raw = request.get_data()
normalized = normalize_json_body(raw)
signature = request.headers.get('X-Signature')
expected = hmac.new(SECRET_KEY, normalized, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
3. Avoid ambiguous header handling and enforce strict message boundaries
Do not rely on headers like Content-Length alone when a proxy may reinterpret them. Prefer a single source of truth for body boundaries and ensure the HMAC covers the entire message unit that determines routing. Do not compute HMAC on partial or reformatted data in different layers.
@app.before_request
def strict_verification():
# Reject requests with conflicting Transfer-Encoding or suspicious dual Content-Length
te = request.headers.get('Transfer-Encoding', '')
cl = request.headers.get('Content-Length', '')
if te and 'chunked' in te.lower() and cl:
abort(400, 'Conflicting Transfer-Encoding and Content-Length')
# Verify HMAC as shown above
verify_hmac()
4. Align proxy and application on request interpretation
Configure your frontend proxy to either strip smuggling-prone headers before forwarding or ensure it forwards the raw request unchanged so Flask can apply consistent parsing. Document and test the exact header and body format expectations to prevent subtle mismatches that HMAC validation alone cannot detect.
| Action | Safe approach | Risky approach |
|---|---|---|
| HMAC input | Raw body from request.get_data() | Partially parsed JSON or form data |
| Header handling | Reject conflicting Transfer-Encoding + Content-Length | Rely on proxy to fix ambiguous headers silently |
| Normalization | Same normalization rules on proxy and app, or none at all | Different normalization layers without coordination |