{"id":"CVE-2026-46715","aliases":["GHSA-97r5-pg8x-p63p","PYSEC-2026-2478"],"title":"Flask-Security-Too OAuth reauthentication freshness bypass via cross-   user OAuth identity acceptance","summary":"Flask-Security-Too OAuth reauthentication freshness bypass via cross-   user OAuth identity acceptance","severity":"medium","vendor":"flask-security-too","product":"flask-security-too","ecosystem":"pip","affected":["flask-security-too >= 5.8.0, < 5.8.1"],"patched":["flask-security-too 5.8.1"],"published":"2026-05-22","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-97r5-pg8x-p63p","references":[{"url":"https://github.com/pallets-eco/flask-security/security/advisories/GHSA-97r5-pg8x-p63p"},{"url":"https://github.com/pallets-eco/flask-security"}],"tags":["osv","pip"],"ingestedAt":"2026-07-13T18:57:55.743Z","epss":0.00488,"epssPercentile":0.41109,"slug":"CVE-2026-46715","body":"## Overview\n\n### Summary\n\n  Flask-Security-Too 5.8.0's OAuth reauthentication flow can mark a\n  session as fresh after verifying an OAuth account that belongs to a\n  different user.\n\n  If an attacker can operate an already-authenticated but stale victim\n  session, they can complete OAuth verification using their own OAuth\n  identity. The victim session is then treated as recently\n  reauthenticated, allowing freshness-protected account actions to\n  proceed. This was reproduced against the built-in `/change-username`\n  route.\n\n  ### Details\n\n  The issue is in the OAuth verification callback.\n\n  `_oauth_response_common()` resolves the OAuth provider identity to a\n  Flask-Security user:\n\n  - `flask_security/oauth_glue.py:101-108`\n\n  `oauth_verify_response()` then accepts any resolved user and updates\n  the current session freshness timestamp:\n\n  - `flask_security/oauth_glue.py:182-214`\n  - `flask_security/oauth_glue.py:201-204`\n\n  The missing check is that the OAuth-resolved user must match the\n  current authenticated session user. In the failing case:\n\n  - current session user: `victim@example.com`\n  - OAuth verified user: `attacker@example.com`\n  - session marked fresh: yes\n\n  So the attacker is not logging in as the victim, but they are\n  satisfying the victim session's reauthentication requirement with a\n  different account.\n\n  ### PoC\n\n  Tested version:\n\n  - `Flask-Security-Too 5.8.0`\n  - tag `5.8.0`\n  - commit `08288dff6907e413d848a16aaf43fc2c2b2a3b72`\n\n Used a minimal Flask app with:\n\n  ```python\n  SECURITY_OAUTH_ENABLE = True\n  SECURITY_OAUTH_BUILTIN_PROVIDERS = [\"github\"]\n  SECURITY_FRESHNESS = timedelta(seconds=1)\n  SECURITY_FRESHNESS_GRACE_PERIOD = timedelta(seconds=0)\n  SECURITY_USERNAME_ENABLE = True\n  SECURITY_CHANGE_USERNAME = True\n\n  The OAuth provider was replaced with a localhost mock provider\n  returning attacker@example.com. This avoids hitting a live third-party\n  provider while still exercising Flask-Security-Too's real OAuth\n  verification handler.\n\n  Reproduction steps:\n\n  1. Log in as victim@example.com.\n  2. Wait until the session is no longer fresh.\n  3. Confirm POST /change-username is blocked with 401 and\n     reauth_required=true.\n  4. Start OAuth verification with POST /login/oauth-verify-start/\n     github.\n  5. Complete the callback with an OAuth identity for\n     attacker@example.com.\n  6. Confirm the session is still for victim@example.com, but fs_paa has\n     been updated.\n  7. Retry POST /change-username.\n  8. The victim user's username is changed successfully.\n\n  Observed result:\n\n  {\n    \"pre_bypass_status\": 401,\n    \"pre_bypass_reauth_required\": true,\n    \"attacker_identity\": \"attacker@example.com\",\n    \"oauth_verify_response_status\": 302,\n    \"post_bypass_change_username_status\": 200,\n    \"final_email\": \"victim@example.com\",\n    \"final_username\": \"victimowned1777878574\",\n    \"direct_impact_verified\": true\n  }\n\n  Note: CSRF was disabled in the local harness only to keep the test\n  focused on the reauthentication check. This is not a CSRF bypass\n  report.\n\n  This bypasses Flask-Security-Too's freshness/reauthentication\n  boundary.\n\n  Applications using OAuth verification together with freshness-\n  protected account operations may allow a stale victim session to be\n  refreshed using a different user's OAuth account. In my test, this\n  allowed the victim account's username to be changed through Flask-\n  Security-Too's built-in /change-username route.\n\n  A likely fix is to reject OAuth verification unless the resolved OAuth\n  user matches current_user before updating session[\"fs_paa\"].\n\n## Affected packages\n\n- `flask-security-too >= 5.8.0, < 5.8.1`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `flask-security-too 5.8.1`","depth":"sunlit","depthScore":28,"depthScoreParts":{"impact":27.5,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}