Session Fixation in Flask with Bearer Tokens
Session Fixation in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Session fixation occurs when an application allows an attacker to force a user’s session identifier to a known value. In Flask, this traditionally maps to the session cookie (e.g., session from flask), but the concept extends to token-based flows. When Bearer Tokens are used for authentication—whether passed via the Authorization header or stored in a client-side cookie/local storage—the application must ensure that a fresh token is issued after authentication and that pre-authentication tokens are not accepted post-login.
With Bearer Tokens, fixation risk appears in several concrete patterns:
- Stateless APIs where a client-supplied token (e.g., from a previous session or an attacker-controlled browser) is accepted without verification of authentication state. If an endpoint issues a new access token but does not invalidate the old one, an attacker can reuse the known token after the victim authenticates.
- Cookies storing Bearer Tokens (e.g.,
Authorization: Bearer <token>set via JS) that are not bound to a post-login rotation. Flask apps that set a cookie token without regenerating it on login leave the session tied to the attacker-chosen value. - Failure to tie the token to the authenticated user identity (e.g., missing or weak binding between token claims and server-side session state), enabling horizontal privilege escalation (BOLA/IDOR) when a fixed token is reused across users.
For example, consider a Flask API that accepts Bearer Tokens via the Authorization header but does not enforce re-authentication or token rotation at login:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Insecure: accepts a client-supplied token and reuses it post-login
@app.route('/login', methods=['POST'])
def login_insecure():
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'missing token'}), 401
# Vulnerable: attacker can set token before login; app reuses it
return jsonify({'access_token': token, 'message': 'logged in'})
In this pattern, an attacker can craft a link that sets a known token (e.g., via a malicious page or network-level injection). When the victim logs in, the server returns the same token, allowing the attacker to hijack the session. Even when tokens are generated server-side, failing to rotate them on privilege change (e.g., after MFA or role elevation) retains the fixed identifier.
Another common anti-pattern is mixing cookie-based session identifiers with Bearer Tokens without clear boundary enforcement. If Flask’s session cookie is used for web UI and a separate Bearer Token is used for API calls, the two channels must be synchronized and invalidated consistently. Otherwise, an attacker can rely on the fixed channel (e.g., the cookie) while the API channel remains unchanged, bypassing intended protections.
Mapping this to the OWASP API Security Top 10, session fixation aligns with Broken Object Level Authorization (BOLA) and authentication weaknesses. A scanner that inspects authentication and token handling—such as one that runs 12 security checks in parallel including Authentication and BOLA/IDOR—can surface these issues by correlating runtime behavior with OpenAPI/Swagger specifications and runtime responses.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring token independence, rotation, and binding to authenticated identity. Below are concrete, secure patterns for Flask APIs that use Bearer Tokens.
1. Rotate token on login: Always issue a new token after successful authentication and invalidate any pre-authentication token. Do not simply echo a client-supplied value.
import secrets
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory store for illustration; use a secure store in production
token_store = {}
def generate_token():
return secrets.token_urlsafe(32)
@app.route('/login', methods=['POST'])
def login_secure():
credentials = request.json or {}
username = credentials.get('username')
password = credentials.get('password')
# Perform actual auth checks here (e.g., verify password hash)
if not (username == 'alice' and password == 'secret'):
return jsonify({'error': 'invalid credentials'}), 401
# Rotate: generate fresh token and discard any previous value
new_token = generate_token()
token_store[username] = new_token
return jsonify({'access_token': new_token, 'token_type': 'Bearer'})
2. Bind token to identity and scope: Encode minimal claims and validate server-side. Avoid trusting client-provided identifiers post-login.
from flask import Flask, request, jsonify
import jwt
import time
app = Flask(__name__)
SECRET_KEY = 'super-secret-key' # use env var in practice
def create_jwt(username):
payload = {
'sub': username,
'iat': int(time.time()),
'exp': int(time.time()) + 3600,
'scope': 'api:read'
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
@app.route('/protected')
def protected():
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return jsonify({'error': 'missing bearer token'}), 401
token = auth.split(' ', 1)[1]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# Ensure token is bound to identity and scope
return jsonify({'user': payload['sub'], 'scope': payload['scope']})
except jwt.ExpiredSignatureError:
return jsonify({'error': 'token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'invalid token'}), 401
3. Invalidate pre-authentication tokens: If you store tokens client-side (e.g., in cookies or local storage), ensure that login triggers invalidation on the server for any prior tokens associated with the same user.
# Example invalidation on login
@app.route('/login', methods=['POST'])
def login_invalidation():
credentials = request.json or {}
username = credentials.get('username')
# Revoke any existing token for this user
token_store.pop(username, None)
new_token = generate_token()
token_store[username] = new_token
return jsonify({'access_token': new_token})
4. Use Secure, HttpOnly cookies when storing tokens client-side: If you embed Bearer Tokens in cookies, set Secure, HttpOnly, and SameSite attributes to mitigate injection via scripts or insecure channels.
from flask import make_response
@app.route('/set-token')
def set_token_cookie():
resp = make_response(jsonify({'message': 'token set'}))
resp.set_cookie(
'auth_token',
value='Bearer your-secure-token',
httponly=True,
secure=True,
samesite='Strict'
)
return resp
These patterns reduce fixation risk by ensuring tokens are unguessable, rotated on authentication, and validated server-side. They complement broader API security checks such as Authentication, BOLA/IDOR, and Input Validation that can be run automatically in under 15 seconds via scans that include OpenAPI/Swagger analysis with full $ref resolution. For teams that want continuous visibility, the Pro plan supports continuous monitoring and GitHub Action integration to fail builds if risk scores drop below a set threshold.