Session Fixation in Flask with Hmac Signatures
Session Fixation in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Session fixation occurs when an application forces a user to use a session identifier (session ID) that the attacker knows or can set. In Flask, developers sometimes use signed cookies to store session identifiers and rely on HMAC signatures to ensure integrity. While HMAC prevents tampering with the cookie value, it does not prevent the server from accepting an attacker-supplied session ID. If the application does not issue a new signed session cookie after authentication, the user’s session ID remains the one originally provided — for example, set by an attacker via a link or injected script.
Consider a Flask app that creates a signed cookie using HMAC but does not rotate the session identifier on login:
from flask import Flask, request, make_response
import hmac
import hashlib
app = Flask(__name__)
SECRET_KEY = b'super-secret-key'
def sign_session(session_id):
signature = hmac.new(SECRET_KEY, session_id.encode(), hashlib.sha256).hexdigest()
return f'{session_id}.{signature}'
def verify_session(cookie_value):
if '.' not in cookie_value:
return None
session_id, received_sig = cookie_value.rsplit('.', 1)
expected_sig = hmac.new(SECRET_KEY, session_id.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, received_sig):
return None
return session_id
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
# naive credential check
if username == 'admin' and password == 'secret':
session_id = request.cookies.get('session', 'fixed-by-attacker')
response = make_response('Logged in')
response.set_cookie('session', sign_session(session_id))
return response
return 'Invalid credentials', 401
@app.route('/profile')
def profile():
session_id = verify_session(request.cookies.get('session', ''))
if session_id is None:
return 'Unauthorized', 401
return f'Hello {session_id}'
In this example, the session ID is taken directly from the incoming cookie before authentication. Because the app signs whatever session ID it receives and sends it back, an attacker can craft a link with a known session ID (e.g., http://example.com/login?session=ATTACKER_SESSION), trick a victim into logging in, and then reuse the signed session ID to impersonate the victim. HMAC ensures the cookie cannot be altered undetected, but the fixation stems from failing to issue a fresh signed session cookie after successful authentication.
The vulnerability is compounded when the application exposes the session via URLs or relies on unverified origins, increasing the likelihood that an attacker can predict or deliver a specific session ID. Because HMAC only guarantees integrity, not freshness or uniqueness per authenticated session, developers must explicitly rotate the session identifier upon authentication to mitigate fixation.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
To remediate session fixation with HMAC-signed cookies in Flask, ensure that a new signed session cookie is issued immediately after successful authentication. This invalidates any pre-authentication session ID and binds the session to the authenticated user.
Below is a secure implementation that generates a cryptographically random session ID, signs it with HMAC, and sets a new cookie after login:
from flask import Flask, request, make_response
import hmac
import hashlib
import os
app = Flask(__name__)
SECRET_KEY = b'super-secret-key'
def sign_session(session_id):
signature = hmac.new(SECRET_KEY, session_id.encode(), hashlib.sha256).hexdigest()
return f'{session_id}.{signature}'
def verify_session(cookie_value):
if '.' not in cookie_value:
return None
session_id, received_sig = cookie_value.rsplit('.', 1)
expected_sig = hmac.new(SECRET_KEY, session_id.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, received_sig):
return None
return session_id
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
if username == 'admin' and password == 'secret':
# Generate a fresh session ID to prevent fixation
fresh_session_id = os.urandom(16).hex()
response = make_response('Logged in')
response.set_cookie('session', sign_session(fresh_session_id), httponly=True, secure=True, samesite='Lax')
return response
return 'Invalid credentials', 401
@app.route('/profile')
def profile():
session_id = verify_session(request.cookies.get('session', ''))
if session_id is None:
return 'Unauthorized', 401
return f'Hello {session_id}'
Key remediation steps included:
- Generate a new random session ID using
os.urandomafter authentication instead of reusing the incoming cookie value. - Always set the
httponly,secure, andsamesiteflags on the cookie to reduce exposure to client-side script access and cross-site request forgery. - Verify the HMAC signature before using the session ID, ensuring the value has not been altered.
For applications using an OpenAPI spec, you can document the session handling expectations and validate that authentication endpoints explicitly rotate session identifiers. The CLI tool (middlebrick scan <url>) can detect whether authentication flows appear to retain pre-login session identifiers across requests, which is a useful indicator during security assessments.