1 line
4.6 KiB
JSON
1 line
4.6 KiB
JSON
{"schema_version":"1.9.0","id":"GHSA-97r5-pg8x-p63p","published":"2026-05-22T17:48:54Z","modified":"2026-07-13T16:43:10.074737178Z","aliases":["CVE-2026-46715","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\"].","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/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-97r5-pg8x-p63p/GHSA-97r5-pg8x-p63p.json"}}],"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"}],"database_specific":{"cwe_ids":["CWE-287"],"github_reviewed":true,"github_reviewed_at":"2026-05-22T17:48:54Z","nvd_published_at":null,"severity":"MODERATE"}} |