VYPR

Flask Security

by Pallets Eco

Source repositories

CVEs (1)

  • CVE-2026-46715May 22, 2026
    risk 0.00cvss epss

    ### Summary Flask-Security-Too 5.8.0's OAuth reauthentication flow can mark a session as fresh after verifying an OAuth account that belongs to a different user. If an attacker can operate an already-authenticated but stale victim session, they can complete OAuth verification using their own OAuth identity. The victim session is then treated as recently reauthenticated, allowing freshness-protected account actions to proceed. This was reproduced against the built-in `/change-username` route. ### Details The issue is in the OAuth verification callback. `_oauth_response_common()` resolves the OAuth provider identity to a Flask-Security user: - `flask_security/oauth_glue.py:101-108` `oauth_verify_response()` then accepts any resolved user and updates the current session freshness timestamp: - `flask_security/oauth_glue.py:182-214` - `flask_security/oauth_glue.py:201-204` The missing check is that the OAuth-resolved user must match the current authenticated session user. In the failing case: - current session user: `victim@example.com` - OAuth verified user: `attacker@example.com` - session marked fresh: yes So the attacker is not logging in as the victim, but they are satisfying the victim session's reauthentication requirement with a different account. ### PoC Tested version: - `Flask-Security-Too 5.8.0` - tag `5.8.0` - commit `08288dff6907e413d848a16aaf43fc2c2b2a3b72` Used a minimal Flask app with: ```python SECURITY_OAUTH_ENABLE = True SECURITY_OAUTH_BUILTIN_PROVIDERS = ["github"] SECURITY_FRESHNESS = timedelta(seconds=1) SECURITY_FRESHNESS_GRACE_PERIOD = timedelta(seconds=0) SECURITY_USERNAME_ENABLE = True SECURITY_CHANGE_USERNAME = True The OAuth provider was replaced with a localhost mock provider returning attacker@example.com. This avoids hitting a live third-party provider while still exercising Flask-Security-Too's real OAuth verification handler. Reproduction steps: 1. Log in as victim@example.com. 2. Wait until the session is no longer fresh. 3. Confirm POST /change-username is blocked with 401 and reauth_required=true. 4. Start OAuth verification with POST /login/oauth-verify-start/ github. 5. Complete the callback with an OAuth identity for attacker@example.com. 6. Confirm the session is still for victim@example.com, but fs_paa has been updated. 7. Retry POST /change-username. 8. The victim user's username is changed successfully. Observed result: { "pre_bypass_status": 401, "pre_bypass_reauth_required": true, "attacker_identity": "attacker@example.com", "oauth_verify_response_status": 302, "post_bypass_change_username_status": 200, "final_email": "victim@example.com", "final_username": "victimowned1777878574", "direct_impact_verified": true } Note: CSRF was disabled in the local harness only to keep the test focused on the reauthentication check. This is not a CSRF bypass report. This bypasses Flask-Security-Too's freshness/reauthentication boundary. Applications using OAuth verification together with freshness- protected account operations may allow a stale victim session to be refreshed using a different user's OAuth account. In my test, this allowed the victim account's username to be changed through Flask- Security-Too's built-in /change-username route. A likely fix is to reject OAuth verification unless the resolved OAuth user matches current_user before updating session["fs_paa"].