Flask-Security-Too OAuth Freshness Bypass: When Verifying the Wrong User Still Counts
CVE analysis of Flask-Security-Too 5.8.0's OAuth reauthentication bypass where verifying a different user's OAuth identity marks the session as fresh, enabling privilege escalation.
Why This Should Scare You
Imagine you're running a Flask app with sensitive admin actions — changing email addresses, resetting 2FA, modifying billing info. You've done the right thing: you implemented reauthentication freshness checks so that even if someone has an active session, they need to prove they're still the right person before touching anything dangerous.
Now imagine all of that is worthless because an attacker can satisfy your freshness check by verifying an OAuth account that belongs to a completely different user.
That's exactly what dropped two days ago in GHSA-97r5-pg8x-p63p, affecting Flask-Security-Too version 5.8.0. The OAuth reauthentication flow — the thing that's supposed to add a layer of protection — will happily mark a session as "fresh" after verifying an OAuth identity that doesn't even belong to the current session user. The gate is there. It just doesn't check whose key you're using.
This isn't a theoretical concern. Any application using Flask-Security-Too's OAuth reauthentication to protect sensitive operations is potentially wide open to privilege escalation.
What Happened
Flask-Security-Too is a widely-used security extension for Flask applications. It handles authentication, authorization, session management, and — critically — reauthentication flows. Version 5.8.0 introduced (or exposed) a flaw in how OAuth-based reauthentication validates identity.
The vulnerability lives in the OAuth reauthentication freshness mechanism. In Flask-Security-Too, "freshness" is a concept where certain sensitive operations require the user to re-prove their identity, even if they already have a valid session. Think of it like sudo — you're logged in, but you need to type your password again before doing something dangerous.
When OAuth is used as the reauthentication method, the flow should verify that the OAuth identity returned by the provider matches the currently logged-in user. The bug? It doesn't perform that cross-check. It verifies that an OAuth authentication succeeded, but not that it succeeded for the right user.
The advisory was published on 2026-05-22 under GitHub Security Advisory GHSA-97r5-pg8x-p63p. The affected version is 5.8.0 of the Flask-Security-Too package. The attack vector is network-based, requires low complexity, and the attacker needs to have at least a low-privilege authenticated session.
This is a logic flaw, not a memory corruption or injection bug. It's the kind of vulnerability that passes every unit test because each individual step works correctly — it's the relationship between steps that's broken.
Technical Deep-Dive: How the Bypass Works
Let's break down the normal flow first. When a user tries to perform a sensitive action, Flask-Security-Too checks if their session is "fresh." If it's not, they get redirected to reauthenticate. With OAuth, that means going through the OAuth dance again — redirect to provider, authenticate, callback, done.
Here's what the vulnerable pattern looks like conceptually in the reauthentication handler:
@app.route('/reauthenticate/oauth/callback')
def oauth_reauth_callback():
# Get the OAuth response from the provider
oauth_response = oauth_provider.authorize_access_token()
# Verify that OAuth authentication succeeded
user_info = oauth_provider.get_userinfo()
if user_info:
# BUG: This marks the session as fresh without checking
# if user_info belongs to current_user
session['fs_fresh'] = True
session['fs_fresh_time'] = datetime.utcnow()
return redirect(request.args.get('next', '/'))
return abort(401)See the problem? The code confirms that someone authenticated via OAuth. It never asks: "Is this the same person who's currently logged in?" The user_info from the OAuth provider could belong to any valid OAuth account.
Here's what the correct implementation should look like:
@app.route('/reauthenticate/oauth/callback')
def oauth_reauth_callback():
oauth_response = oauth_provider.authorize_access_token()
user_info = oauth_provider.get_userinfo()
if user_info:
# CORRECT: Verify the OAuth identity matches the current session user
oauth_identity = lookup_oauth_identity(user_info['sub'])
if oauth_identity and oauth_identity.user_id == current_user.id:
session['fs_fresh'] = True
session['fs_fresh_time'] = datetime.utcnow()
return redirect(request.args.get('next', '/'))
else:
# OAuth identity doesn't belong to current user - reject
abort(403)
return abort(401)The attack flow works like this:
- Attacker has a low-privilege account (User A) with an active session
- Attacker triggers a sensitive action that requires reauthentication
- Application redirects to OAuth reauthentication flow
- Attacker completes the OAuth flow using their own OAuth account (which may be linked to a different user, or even an unlinked account)
- The callback handler sees a successful OAuth authentication and marks the session as fresh
- Attacker's session (as User A) is now "fresh" — they can perform sensitive actions
The root cause is a violation of a fundamental security principle: authentication verification must be bound to the identity being verified. The code treats OAuth reauthentication as a binary yes/no ("did someone authenticate?") when it should be a three-way check ("did the correct person authenticate as themselves?").
This is particularly insidious because the attacker doesn't need to compromise anyone else's OAuth account. They just need any valid OAuth identity that the provider will authenticate. They can use their own secondary account, a throwaway account, or even an account they control that's linked to a different user in the application.
An edge case worth considering: what if the application allows multiple OAuth providers? The attacker might authenticate with Provider B while the target user only has Provider A linked. If the freshness check doesn't also verify the provider matches, that's another bypass vector on top of the identity mismatch.
How to Detect If You're Affected
First, check if you're running the vulnerable version. This one-liner will tell you:
pip show Flask-Security-Too | grep -E "^Version:"If it returns 5.8.0, you're in the danger zone.
Next, check if your application actually uses OAuth reauthentication. Search your codebase for the relevant configuration and route patterns:
grep -rn "oauthglue\|oauth_reauth\|SECURITY_OAUTH\|freshness" --include="*.py" .If you're using Flask-Security-Too's built-in OAuth reauthentication (via the oauthglue mechanism or similar), and you haven't added your own identity verification on top, you're vulnerable.
You can also test this directly. If you have a staging environment, try this: log in as User A, trigger a freshness-protected action, and when redirected to OAuth reauthentication, authenticate with a different OAuth account (User B's credentials or a separate test account). If the action succeeds, you've confirmed the bypass:
# Step 1: Get a session as user A
curl -c cookies.txt -X POST https://your-app.example/login \
-d "email=usera@example.com&password=testpass"
# Step 2: Try to access a freshness-protected endpoint
curl -b cookies.txt -v https://your-app.example/sensitive-action
# Should redirect to reauthentication
# Step 3: Complete OAuth flow with a DIFFERENT user's OAuth account
# If the sensitive action succeeds after this, you're vulnerableThe manual test is the definitive check. Automated scanning won't catch this because it's a logic flaw, not a signature-based vulnerability.
Impact Analysis
The blast radius here depends entirely on what your application protects behind freshness checks. Common patterns include:
- Email/password changes — attacker changes the account email, locks out the real user
- 2FA enrollment/removal — attacker disables 2FA on a target account
- API key generation/rotation — attacker generates keys with the victim's permissions
- Billing/payment modifications — attacker redirects payments or makes purchases
- Admin actions — if admin panels use freshness checks, this becomes a full privilege escalation
The prerequisite is that the attacker needs an authenticated session. But here's the thing — that's a low bar. In many applications, anyone can sign up. So the attack chain is: create account → log in → bypass freshness → perform sensitive actions that should require reauthentication.
Chaining is where this gets really dangerous. Combine this with any session fixation or CSRF vulnerability, and you've got a path from unauthenticated to full account takeover. Even without chaining, if your app has any feature where a low-privilege user can trigger actions on behalf of higher-privilege users (think: support ticket systems, shared workspaces), this freshness bypass turns a minor access control issue into a critical one.
Any Flask application using Flask-Security-Too 5.8.0 with OAuth-based reauthentication is affected. Given Flask-Security-Too's popularity in the Python web ecosystem, that's a non-trivial number of applications.
What to Do About It Right Now
The immediate fix is to upgrade Flask-Security-Too past 5.8.0. Check the project's releases for the patched version:
pip install --upgrade Flask-Security-Too
# Verify the installed version
pip show Flask-Security-Too | grep VersionIf you can't upgrade immediately, implement a manual identity check in your OAuth reauthentication callback. Here's a middleware-style workaround you can drop in:
from flask import session, abort
from flask_login import current_user
def verify_oauth_reauth_identity(oauth_user_info):
"""
Workaround for GHSA-97r5-pg8x-p63p.
Call this in your OAuth reauth callback BEFORE marking session as fresh.
"""
# Look up which user this OAuth identity belongs to
from your_app.models import OAuthIdentity
oauth_identity = OAuthIdentity.query.filter_by(
provider_user_id=oauth_user_info.get('sub')
).first()
if not oauth_identity:
abort(403, description="OAuth identity not linked to any account")
if oauth_identity.user_id != current_user.id:
# This is the attack scenario - someone else's OAuth identity
abort(403, description="OAuth identity does not match current session")
return TrueCall that function before any freshness marking happens in your callback.
For platform-specific mitigations, if you're running behind a reverse proxy, you can add rate limiting on the reauthentication endpoints to slow down exploitation attempts. Here's an nginx config:
location /reauthenticate {
limit_req zone=reauth burst=3 nodelay;
proxy_pass http://flask_app;
}For Vercel deployments, add security headers in your vercel.json to ensure the OAuth callback can't be framed or intercepted:
{
"headers": [
{
"source": "/reauthenticate/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" },
{ "key": "Cache-Control", "value": "no-store, no-cache, must-revalidate" }
]
}
]
}If you're using Cloudflare, consider adding a WAF rule that flags rapid successive hits to your OAuth callback endpoint from the same session but different OAuth state parameters — that's a strong signal of exploitation attempts.
To verify your fix works, repeat the detection test from earlier: authenticate as User A, trigger reauthentication, complete OAuth as User B. You should now get a 403 instead of a successful freshness grant.
The Bigger Picture
This vulnerability is a textbook example of a pattern I see over and over in authentication systems: the "any valid credential" mistake. Developers build a gate, verify that a key works, but forget to check that it's the right key for this door. It's the same class of bug as IDOR, just applied to authentication flows instead of data access.
OAuth reauthentication is particularly prone to this because the OAuth flow is complex — there's a redirect, a callback, state parameters, token exchange. By the time the callback fires, the developer is thinking "did the OAuth flow succeed?" not "did it succeed for the right person?" The complexity of the flow obscures the simplicity of the required check.
Look, if you're building any kind of step-up authentication or reauthentication flow — OAuth or otherwise — the rule is simple: the identity proven must match the identity claiming access. Write that on a sticky note. Put it on your monitor. Every reauthentication callback should have an explicit identity comparison as its first meaningful operation. Not as an afterthought. Not as a "we'll add that later." First. Always.
Related posts
- Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
May 18, 2026 · 9 min