{"id":"PYSEC-2026-2478","summary":"Flask-Security-Too OAuth reauthentication freshness bypass via cross-   user OAuth identity acceptance","details":"### 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\"].","aliases":["CVE-2026-46715","GHSA-97r5-pg8x-p63p"],"modified":"2026-07-13T16:31:46.462054176Z","published":"2026-07-13T15:19:11.494189Z","references":[{"type":"WEB","url":"https://github.com/pallets-eco/flask-security/security/advisories/GHSA-97r5-pg8x-p63p"},{"type":"PACKAGE","url":"https://github.com/pallets-eco/flask-security"},{"type":"PACKAGE","url":"https://pypi.org/project/flask-security-too"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-97r5-pg8x-p63p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46715"}],"affected":[{"package":{"name":"flask-security-too","ecosystem":"PyPI","purl":"pkg:pypi/flask-security-too"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.8.0"},{"fixed":"5.8.1"}]}],"versions":["5.8.0"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/flask-security-too/PYSEC-2026-2478.yaml"}}],"schema_version":"1.7.5"}