Session Fixation in Fastapi with Hmac Signatures
Session Fixation in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Session fixation occurs when an application assigns a user a session identifier before authentication and fails to rotate or validate that identifier after login. In Fastapi applications that use Hmac Signatures for session integrity, fixation can arise when the server embeds a predictable or static value into the signed token and does not bind the signature to a post-authentication context. Hmac Signatures protect integrity but do not in themselves prevent fixation; they only ensure that a tamper-proof session payload is presented.
Consider a Fastapi design where a pre-login session cookie contains a signed payload with a user_id field set to None or a client-supplied value. An attacker can force a victim to use a known session identifier (for example, by sending a link with session_id=attacker_chosen) and, after the victim authenticates, the server validates the Hmac signature and accepts the now-authenticated session because the signature covers the attacker-controlled identifier. Because the signature is valid, the server treats the fixed session as legitimate. Even if the payload includes an Hmac algorithm field such as HS256, the vulnerability is not in the algorithm but in the lack of per-session nonce or binding to a post-login context.
Another scenario involves Fastapi endpoints that issue signed cookies with claims like iat (issued at), exp (expiration), and a static session_kind marker. If the server does not create a new signature-bound session object after authentication—such as by generating a fresh random session_fingerprint and including it in the Hmac payload—an attacker can reuse the signed token. The Hmac signature will still verify, yet the session context remains predictable. This is especially relevant when the signature is computed over a JSON structure that includes identifiers supplied by the client without server-side reconciliation.
In practice, the risk is realized when the application conflates authentication with session continuity without re-signing a new, server-chosen identifier. The OWASP API Top 10 category Broken Object Level Authorization (BOLA) intersects with session fixation when an attacker manipulates object identifiers that are covered by Hmac checks but not rotated after privilege changes. MiddleBrick’s checks for BOLA/IDOR and Unsafe Consumption can surface endpoints where session tokens rely on client-influenced values despite Hmac protection, highlighting the need for server-side rotation and strict binding of signatures to authenticated context.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on ensuring that every authenticated session uses a server-generated, cryptographically random identifier that is bound into the Hmac payload and rotated at login. Below are concrete, realistic code examples for Fastapi that demonstrate a secure pattern.
Secure session creation with Hmac Signatures
Use secrets.token_urlsafe to generate a fresh session handle, embed it with other claims, and sign with a server-side key. Validate the signature, then enforce that the session handle is not client-supplied before authentication.
from fastapi import Fastapi, Request, Response, HTTPException, status
from pydantic import BaseModel
import hmac
import hashlib
import secrets
import time
import json
app = Fastapi()
SECRET_KEY = b'your-secure-server-side-key' # store safely, e.g., env var
def build_session_token(user_id: str, session_handle: str, now: int) -> str:
payload = {
'user_id': user_id,
'session_handle': session_handle,
'iat': now,
'exp': now + 3600 # 1 hour
}
header = {'alg': 'HS256', 'typ': 'JWT-like'}
encoded_header = base64_url_encode(json.dumps(header, separators=(',', ':')))
encoded_payload = base64_url_encode(json.dumps(payload, separators=(',', ':')))
message = f'{encoded_header}.{encoded_payload}'.encode()
signature = hmac.new(SECRET_KEY, message, hashlib.sha256).digest()
encoded_signature = base64_url_encode(signature)
return f'{encoded_header}.{encoded_payload}.{encoded_signature}'
def verify_session_token(token: str) -> dict:
parts = token.split('.')
if len(parts) != 3:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token format')
header_b64, payload_b64, sig_b64 = parts
message = f'{header_b64}.{payload_b64}'.encode()
expected_sig = base64_url_decode(sig_b64)
computed_sig = hmac.new(SECRET_KEY, message, hashlib.sha256).digest()
if not hmac.compare_digest(computed_sig, expected_sig):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid signature')
payload = json.loads(base64_url_decode(payload_b64).decode())
now = int(time.time())
if payload.get('exp', 0) < now:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Token expired')
return payload
@app.post('/login')
async def login(username: str, password: str, response: Response):
# authenticate user (pseudo)
user_id = authenticate_user(username, password)
if not user_id:
raise HTTPException(status_code=401, detail='Bad credentials')
# generate server-side session handle to prevent fixation
session_handle = secrets.token_urlsafe(32)
token = build_session_token(user_id=user_id, session_handle=session_handle, now=int(time.time()))
response.set_cookie(
key='session',
value=token,
httponly=True,
secure=True,
samesite='lax',
max_age=3600
)
return {'ok': True}
@app.get('/profile')
async def profile(request: Request):
token = request.cookies.get('session')
if not token:
raise HTTPException(status_code=401, detail='Missing session')
payload = verify_session_token(token)
# ensure session_handle is server-defined, never echoed from untrusted input
return {'user_id': payload['user_id'], 'session_handle': payload['session_handle']}
def base64_url_encode(data: str) -> str:
import base64
return base64.urlsafe_b64encode(data.encode()).rstrip(b'=').decode()
def base64_url_decode(data: str) -> bytes:
import base64
padding = '=' * (-len(data) % 4)
return base64.urlsafe_b64decode(data + padding)
# Example usage:
# After login, client presents the cookie; server verifies Hmac and uses the embedded session_handle.
Key points in this pattern:
session_handleis generated server-side with high entropy (secrets.token_urlsafe) and never taken from client input.- The Hmac signature covers the entire payload, including the server-generated handle, so an attacker cannot swap in a known identifier even if they can influence other fields.
- On login, a new token is issued; the old token is invalidated by the client discarding the cookie, effectively rotating the session binding.
- Claims like
iatandexpare included to limit token lifetime, reducing the window for fixation use if a token were somehow fixed before authentication.
Middleware validation to reject fixed sessions
Add a lightweight dependency that ensures session_handle is not reused across authentication states and that the token is re-evaluated after login.
from fastapi import Request
import time
SESSION_SEEN = set()
@app.middleware('http')
async def reject_fixed_sessions(request: Request, call_next):
if request.url.path == '/login':
response = await call_next(request)
return response
token = request.cookies.get('session')
if token:
payload = verify_session_token(token)
handle = payload.get('session_handle')
if handle in SESSION_SEEN:
raise HTTPException(status_code=401, detail='Session fixation detected')
SESSION_SEEN.add(handle)
response = await call_next(request)
return response
These measures ensure that Hmac Signatures protect integrity while the session lifecycle explicitly prevents fixation. Compared to alternatives that rely on opaque identifiers without cryptographic binding, this approach aligns with best practices for session management in Fastapi and maps to findings that tools like MiddleBrick may surface under BOLA/IDOR and Unsafe Consumption checks.