GHSA-97R5-PG8X-P63P

Vulnerability from github – Published: 2026-05-22 17:48 – Updated: 2026-05-22 17:48
VLAI
Summary
Flask-Security-Too OAuth reauthentication freshness bypass via cross- user OAuth identity acceptance
Details

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"].

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "Flask-Security-Too"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.8.0"
            },
            {
              "fixed": "5.8.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46715"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-22T17:48:54Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\n  Flask-Security-Too 5.8.0\u0027s 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\u0027s 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\u0027s 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\u0027s 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\u0027s 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\u0027s OAuth account. In my test, this\n  allowed the victim account\u0027s username to be changed through Flask-\n  Security-Too\u0027s 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\"].",
  "id": "GHSA-97r5-pg8x-p63p",
  "modified": "2026-05-22T17:48:54Z",
  "published": "2026-05-22T17:48:54Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [],
  "summary": "Flask-Security-Too OAuth reauthentication freshness bypass via cross-   user OAuth identity acceptance"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…