Overview
In real-world OAuth flows, an attacker can exploit weak handling of the OAuth state parameter to induce actions in a victim’s context. CVE-2025-14546 describes fastapi-sso implementations prior to 0.19.0 where the get_login_url path generates a state value but does not persist or bind it to the user's session. The subsequent verify_and_process step accepts whatever state arrives in the callback without validating it against a trusted local value. This CSRF-like weakness enables an attacker to trick a victim into visiting a malicious callback URL and potentially link the attacker’s account to the victim’s internal account or perform unwanted actions within the victim’s session context. The root problem is a missing per-session binding of the OAuth state, which is a classic form of Broken Function Level Authorization: the server cannot reliably tie the authentication response to the legitimate user session, allowing cross-user misuse through a crafted callback flow. The CVE highlights the concrete risk in the fastapi-sso library when state handling is not tied to a server-side session before 0.19.0.
Affected Versions
fastapi-sso < 0.19.0
Code Fix Example
FastAPI API Security Remediation
Vulnerable pattern (no per-session state binding):
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
import secrets
REDIRECT_URI = 'https://app.example.com/auth/callback'
LOGIN_URL_BASE = 'https://provider.example.com/auth'
app_vuln = FastAPI()
@app_vuln.get('/login')
async def login_vuln():
state = secrets.token_urlsafe(16)
login_url = f"{LOGIN_URL_BASE}?response_type=code&client_id=CLIENT&redirect_uri={REDIRECT_URI}&state={state}"
# State is generated but not persisted per-user/session
return RedirectResponse(login_url)
@app_vuln.get('/auth/callback')
async def callback_vuln(code: str, state: str):
# No server-side check that 'state' matches a stored value for a session
# Exchange 'code' for tokens and establish user session (omitted)
return { 'status': 'ok', 'state_received': state }
Fixed pattern (bind state to user session, verify on callback):
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse, JSONResponse
from starlette.middleware.sessions import SessionMiddleware
import secrets
REDIRECT_URI = 'https://app.example.com/auth/callback'
LOGIN_URL_BASE = 'https://provider.example.com/auth'
app_fix = FastAPI()
app_fix.add_middleware(SessionMiddleware, secret_key='your-very-secure-secret')
@app_fix.get('/login')
async def login_fix(request: Request):
state = secrets.token_urlsafe(16)
# Persist the state per-user in server-side session
request.session['oauth_state'] = state
login_url = f"{LOGIN_URL_BASE}?response_type=code&client_id=CLIENT&redirect_uri={REDIRECT_URI}&state={state}"
return RedirectResponse(login_url)
@app_fix.get('/auth/callback')
async def callback_fix(request: Request, code: str, state: str):
# Validate callback state against the stored per-session value
if request.session.get('oauth_state') != state:
return JSONResponse({'error': 'invalid_state'}, status_code=400)
# Consume/clear the state after use
request.session.pop('oauth_state', None)
# Proceed to exchange 'code' for tokens and establish user session (omitted)
return { 'status': 'ok' }