Insecure Deserialization in Flask with Hmac Signatures
Insecure Deserialization in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
In Flask applications that use signed cookies or query parameters, developers commonly serialize an object (such as a user role or permissions dict), then produce an HMAC signature to assert integrity. The pattern is: data = base64_urlsafe(serialize(payload)), signature = hmac.new(secret, data, sha256), and the client receives data.signature. The vulnerability arises when an attacker can supply a serialized object that the server deserializes without strict type and schema validation before verifying the HMAC. Because the signature only protects integrity (the data was produced by the key holder), not authenticity of the deserialization path, an attacker can craft malicious serialized bytes that, once deserialized, execute code or change behavior. Insecure deserialization bugs (e.g., Python pickle or yaml.load with Loader=yaml.Loader) allow an attacker who can control the serialized content to invoke arbitrary constructors. Even with Hmac Signatures confirming the payload hasn’t been tampered with, the server may still deserialize attacker-controlled data if it trusts its own signature too early. For example, if Flask uses itsdangerous to sign a cookie but the app later reconstructs a complex object via pickle.loads on the deserialized field, the signature prevents modification but does not prevent the server from processing malicious payloads it generated itself or from formats it accepts. This becomes critical when endpoints reflect deserialized data into templates, ORM queries, or dynamic function calls, enabling injection chains such as CVE-2023-45232-like gadget chains in Python objects. The risk is compounded when the Hmac key is leaked or when the framework’s session cookie is not bound to a strict deserialization policy, effectively turning a tamper-proof channel into an execution channel.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
Remediation focuses on never deserializing untrusted or complex objects, using type-safe structures, and validating schema before any business logic. Prefer JSON with strict schemas instead of pickle or yaml.Loader. Below are concrete Flask examples that keep Hmac Signatures but avoid insecure deserialization.
Example 1: Safe signed payload with JSON and a strict schema
import json
import hmac
import hashlib
import base64
from flask import Flask, request, make_response
from jsonschema import validate, ValidationError
app = Flask(__name__)
SECRET = b'super-secret-key'
SCHEMA = {
"type": "object",
"properties": {
"user_id": {"type": "integer"},
"role": {"type": "string", "enum": ["user", "admin"]}
},
"required": ["user_id", "role"]
}
def sign(data: bytes) -> str:
signature = hmac.new(SECRET, data, hashlib.sha256).digest()
return base64.urlsafe_b64encode(signature).rstrip(b'=').decode()
def verify(data: bytes, received_sig: str) -> bool:
expected = hmac.new(SECRET, data, hashlib.sha256).digest()
received = base64.urlsafe_b64decode(received_sig + '=' * (-len(received_sig) % 4))
return hmac.compare_digest(expected, received)
@app.route('/set-payload')
def set_payload():
payload = json.dumps({"user_id": 42, "role": "user"}).encode()
token = sign(payload)
response = make_response('ok')
response.set_cookie('session', f'{base64.urlsafe_b64encode(payload).rstrip(b"=").decode()}.{token}')
return response
@app.route('/read-payload')
def read_payload():
cookie = request.cookies.get('session', '')
if '.' not in cookie:
return 'invalid', 400
data_b64, sig = cookie.rsplit('.', 1)
data = base64.urlsafe_b64decode(data_b64 + '=' * (-len(data_b64) % 4))
if not verify(data, sig):
return 'bad signature', 403
try:
payload = json.loads(data)
validate(instance=payload, schema=SCHEMA)
except (json.JSONDecodeError, ValidationError):
return 'invalid payload', 400
# Safe to use payload['user_id'] and payload['role']
return f'user={payload["user_id"]} role={payload["role"]}'
Example 2: Using itsdangerous with JSON serializer instead of pickle
from flask import Flask, jsonify
from itsdangerous import URLSafeTimedSerializer
import json
app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret-key'
serializer = URLSafeTimedSerializer(app.secret_key, salt='session-v1')
# Serialize only JSON-safe primitives
def make_token(user_id: int, role: str) -> str:
payload = {'user_id': user_id, 'role': role}
return serializer.dumps(payload) # uses JSON internally
def verify_token(token: str, max_age=3600):
try:
return serializer.loads(token, max_age=max_age)
except Exception:
return None
@app.route('/profile')
def profile():
token = request.cookies.get('token')
data = verify_token(token)
if not data:
return 'invalid or expired', 403
# data is already a dict of primitives; safe to use
return jsonify(data)
Key remediation practices
- Never use
pickle,yaml.loadwithLoader=yaml.Loader, or any dynamic deserialization on data that may originate from the client. - Keep the Hmac secret rotation policy and store keys outside the codebase.
- Validate and sanitize all fields after deserialization; apply the principle of least privilege to decoded values.
- Use strict timeouts and size limits on cookies and request bodies to reduce abuse surface.