{"id":"CVE-2026-46715","aliases":["GHSA-97r5-pg8x-p63p","PYSEC-2026-2478"],"url":"https://o3.security/vulnerability/CVE-2026-46715","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\"].","published":"2026-07-20T17:10:27.014Z","modified":"2026-08-12T03:51:21.460426626Z","cvss":null,"epss":{"score":0.00279,"percentile":0.20464,"asOf":"2026-08-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"flask-security-too","fixedVersion":"5.8.1"}],"fix":{"url":"https://github.com/pallets-eco/flask-security/commit/8e69f3a94a463c0e8ddc46e31743717559fd8d48","label":"pallets-eco/flask-security@8e69f3a"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46715.json"},{"type":"ADVISORY","url":"https://github.com/pallets-eco/flask-security/security/advisories/GHSA-97r5-pg8x-p63p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46715"},{"type":"FIX","url":"https://github.com/pallets-eco/flask-security/commit/8e69f3a94a463c0e8ddc46e31743717559fd8d48"},{"type":"PACKAGE","url":"https://github.com/pallets-eco/flask-security"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:21.460426626Z"}}