{"answers":{"affected":"2 open source packages ship an affected version, including Flask-Security-Too and flask-security-too.","exploited":"There is no known exploitation of GHSA-f66q-9rf6-8795 in the wild and no public exploit code.","fix":"No fix has been published for GHSA-f66q-9rf6-8795 yet. Limit exposure of the affected software until one is available.","what":"GHSA-f66q-9rf6-8795 is an authentication bypass vulnerability in Flask-Security-Too, published on 7 Jul 2026. Any signed-in user can exploit it over the network, and only if a user opens a file or follows a link."},"fix_prompt":"Check this codebase for GHSA-f66q-9rf6-8795 and fix it if it is affected.\n\nWhat it is: GHSA-f66q-9rf6-8795 is an authentication bypass vulnerability in Flask-Security-Too, published on 7 Jul 2026. Any signed-in user can exploit it over the network, and only if a user opens a file or follows a link.\nExploitation: There is no known exploitation of GHSA-f66q-9rf6-8795 in the wild and no public exploit code.\n\nAffected packages and fixed versions:\n- Flask-Security-Too (PyPI): affected from 5.8.0 up to 5.8.1; no fix listed\n- flask-security-too (PyPI): affected from 5.8.0 up to 5.8.1; no fix listed\n\nTask:\n1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.\n2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.\n3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.\n4. Run the tests and summarise what changed and why.\n5. If nothing in the project is affected, say so and change nothing.\n\nReference: https://insights.sbomify.com/GHSA-f66q-9rf6-8795/ (Markdown: https://insights.sbomify.com/GHSA-f66q-9rf6-8795/index.md)","id":"GHSA-f66q-9rf6-8795","markdown":"https://insights.sbomify.com/GHSA-f66q-9rf6-8795/index.md","name":"GHSA-f66q-9rf6-8795","record":{"aliases":[],"analysis":{"component_type":"library","fix_type":"patch","impacts":["security_bypass"],"network_reachable":"yes","non_default_config":false,"product_category":"Flask security library","requires_auth":"low_privilege","summary":"An authenticated attacker can bypass WebAuthn reauthentication freshness checks by submitting their own credential into a victim's session, allowing unauthorized sensitive actions.","user_interaction":"required","vuln_class":"auth_bypass"},"analysis_generated_by":"qwen3.8-flash-next","assigner":null,"cpe":[],"cvss":{"score":null,"severity":null,"source":null,"vector":null,"version":null},"cwes":[],"description":"### Summary\n\nFlask-Security-Too 5.8.0 and 5.8.1 mark a session as reauthentication-fresh after processing a WebAuthn assertion whose proven credential belongs to a different user than the currently authenticated session user. The check that `GHSA-97r5-pg8x-p63p` added on the OAuth reauthentication path (`user.email == current_user.email`) is missing on the WebAuthn reauthentication path. An attacker who owns any WebAuthn credential registered to any account on the deployment can satisfy a victim session's freshness gate by submitting their own WebAuthn proof into the victim session.\n\n### Affected versions\n\n`Flask-Security-Too` `\u003e= 5.8.0, \u003c= 5.8.1` (current `main` commit `5c44c76e33a20b67d02115e26d2da4bab18c094e`).\n`GHSA-97r5-pg8x-p63p` (published 2026-05-22) shipped its fix in 5.8.1 only on `oauth_glue.py`; `webauthn.py` was not touched and remains exploitable in 5.8.1.\n\n### Privilege required\n\nAuthenticated attacker on the same Flask-Security deployment, owning at least one WebAuthn credential of any usage (`first` / `secondary` / verify) that is registered to their own account. The attacker also needs the ability to drive HTTP requests against the WebAuthn endpoints inside the victim session (e.g. a separate gadget such as CSRF + cookie-based auth, an XSS that doesn't reach the cookie itself but can move the session through endpoints, or an existing session-fixation gadget; or the rarer but easier case of an attacker who has direct access to the victim's not-yet-fresh session via a shared browser). The point of the freshness gate is to defend exactly that \"I have the session but it isn't fresh enough to do sensitive things\" position, so any context in which freshness would have protected the victim is also the context in which this bypass matters.\n\n### Vulnerable code\n\n[`flask_security/webauthn.py:846-889`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/flask_security/webauthn.py#L846-L889) (commit\n`5c44c76e33a20b67d02115e26d2da4bab18c094e`):\n\n```python\n@auth_required(lambda: cv(\"API_ENABLED_METHODS\"))\ndef webauthn_verify_response(token: str) -\u003e ResponseValue:\n    form = t.cast(\n        WebAuthnSigninResponseForm, build_form_from_request(\"wan_signin_response_form\")\n    )\n\n    expired, invalid, state = check_and_get_token_status(\n        token, \"wan\", get_within_delta(\"WAN_SIGNIN_WITHIN\")\n    )\n    ...\n    form.challenge = state[\"challenge\"]\n    form.user_verification = state[\"user_verification\"]\n    form.is_secondary = False\n    form.is_verify = True\n\n    if form.validate_on_submit():\n        # update last use and sign count\n        after_this_request(view_commit)\n        assert form.cred\n        assert form.user\n        form.cred.lastuse_datetime = _security.datetime_factory()\n        form.cred.sign_count = form.authentication_verification.new_sign_count\n        _datastore.put(form.cred)\n\n        # verified - so set freshness time.\n        session[\"fs_paa\"] = time.time()\n        ...\n```\n\n[`flask_security/webauthn.py:276-308`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/flask_security/webauthn.py#L276-L308) (the form's `validate()`):\n\n```python\ndef validate(self, **kwargs: t.Any) -\u003e bool:\n    if not super().validate(**kwargs):\n        return False  # pragma: no cover\n    ...\n    try:\n        auth_cred = parse_authentication_credential_json(self.credential.data)\n    except (...):\n        ...\n        return False\n\n    # Look up credential Id (raw_id) and user. 7.2.6/7\n    self.cred = _datastore.find_webauthn(credential_id=auth_cred.raw_id)\n    ...\n    # This shouldn't be able to happen if datastore properly cascades delete\n    self.user = _datastore.find_user_from_webauthn(self.cred)\n```\n\n`self.user` is resolved from the attacker-controlled `credential_id` and is never compared to `current_user`. The state token issued by `_signin_common` ([`webauthn.py:589-622`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/webauthn.py#L589-L622)) carries only `{challenge, user_verification}`, so state tokens are not bound to any user and replay portably across sessions:\n\n```python\ndef _signin_common(user: UserMixin | None, usage: list[str]) -\u003e tuple[t.Any, str]:\n    ...\n    state = {\n        \"challenge\": challenge,\n        \"user_verification\": uv,\n    }\n    ...\n    state_token = t.cast(str, _security.wan_serializer.dumps(state))\n    return o_json, state_token\n```\n\nContrast with the patch in [`oauth_glue.py:211`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/oauth_glue.py#L211) that `GHSA-97r5-pg8x-p63p` shipped:\n\n```python\nnext_loc = session.pop(\"fs_oauth_next\", None)\nif user and user.email == current_user.email:\n    # verified - so set freshness time.\n    session[\"fs_paa\"] = time.time()\n```\n\nThat `user.email == current_user.email` clamp is the missing check on the WebAuthn side.\n\n### How input reaches the sink\n\n1. Attacker logs in to their own account and registers their own WebAuthn\n   credential (call it `cred_attacker`). They retain a copy of any valid\n   `navigator.credentials.get()` assertion JSON produced by their authenticator\n   (one signature is enough; can also be produced fresh on demand per request).\n2. Attacker holds, or gets, a victim session in a state where `fs_paa` is past\n   `FRESHNESS`. The victim is authenticated as themselves; the gate stops them\n   from invoking freshness-protected business endpoints (`/change`,\n   `/change-username`, `/wf-add`, `/us-setup`, anything decorated with\n   `@auth_required(within=...)`).\n3. The victim session calls `POST /wan-verify` and receives a `wan_state`\n   token. The state token has no user binding.\n4. Attacker submits an assertion that proves possession of `cred_attacker`,\n   inside the victim session, to `POST /wan-verify/\u003cwan_state\u003e`.\n5. `WebAuthnSigninResponseForm.validate` resolves `form.user` to the attacker\n   account from `find_user_from_webauthn(self.cred)`, signs/verifies the\n   assertion against the (attacker-controlled) public key it stored at\n   registration time, and returns `True`. The user-handle check on\n   `auth_cred.response.user_handle` (if present) compares against\n   `self.user.fs_webauthn_user_handle`, i.e. it compares attacker user-handle\n   to attacker user, so it passes trivially.\n6. `webauthn_verify_response` then writes `session[\"fs_paa\"] = time.time()`.\n   The session user is unchanged (still the victim) but the freshness clock\n   is reset by a cryptographic proof of the attacker's authenticator.\n7. Any subsequent `@auth_required(within=...)` endpoint now succeeds inside\n   the victim session.\n\n### End-to-end reproduction\n\nReproduction is an in-process Flask test client driving the published wheel (`pip install Flask-Security-Too==5.8.0`, also re-run against 5.8.1 since `GHSA-97r5-pg8x-p63p`'s fix shipped with that release only touched `oauth_glue.py`). The full transcript is in the Proof of concept section below; here is the boot recipe:\n\n```bash\npython3.12 -m venv venv\nsource venv/bin/activate\npip install --quiet 'Flask-Security-Too==5.8.0' Flask-SQLAlchemy webauthn email-validator argon2_cffi\npython poc.py\n```\n\nCaptured run-time output (5.8.0 path):\n\n```\n=== Submit BOB's WebAuthn assertion to Alice's /wan-verify-response ===\n  cross-user assertion status: 200\n  alice fs_uniquifier in session AFTER: '408245d132bc4213a55606c46f40e038'   # still Alice\n  fs_paa BEFORE: 1779582282.550872\n  fs_paa AFTER : 1779585882.615287                                            # advanced\n=== Demonstrate impact: /sensitive (freshness-protected) accepted ===\n  /sensitive after cross-user verify status: 200\n```\n\nRe-run against 5.8.1 produces the same `200` on the cross-user assertion and the same `200` on the freshness-gated endpoint, confirming that the patch for `GHSA-97r5-pg8x-p63p` did not extend to the WebAuthn path.\n\n### Proof of concept\n\nMocked WebAuthn fixtures (`REG_DATA_UV`, `SIGNIN_DATA_UV`, `REG_DATA1`","ecosystems":["PyPI"],"exploits":{"exploitdb":null,"first_seen":null,"github_poc":null,"max_poc_stars":null,"metasploit":null,"nuclei":null,"refs":[],"tier":null},"has_fix":false,"headline":"Flask-Security-Too: WebAuthn reauthentication freshness bypass via cross-user assertion","id":"GHSA-f66q-9rf6-8795","indexable":true,"kev":null,"kind":"advisory","links":{"ecosystem":"PyPI","product":null,"products":[],"type":"auth_bypass","year":"2026"},"n_packages":2,"n_references":null,"packages":[{"ecosystem":"PyPI","fixed":[],"introduced":["5.8.0"],"last_affected":["5.8.1"],"package":"Flask-Security-Too","purl":null,"source":"GHSA-f66q-9rf6-8795"},{"ecosystem":"PyPI","fixed":[],"introduced":["5.8.0"],"last_affected":["5.8.1"],"package":"flask-security-too","purl":"pkg:pypi/flask-security-too","source":"GHSA-f66q-9rf6-8795"}],"products":null,"published":"2026-07-07","related":[],"sources":[{"class":"attribution","licence":"CC BY 4.0","name":"GitHub Advisory Database","prefix":"GHSA"}],"ssvc":{"automatable":null,"exploitation":null,"technical_impact":null},"state":null,"updated":null,"vendors":null},"summary":"An authenticated attacker can bypass WebAuthn reauthentication freshness checks by submitting their own credential into a victim's session, allowing unauthorized sensitive actions.","url":"https://insights.sbomify.com/GHSA-f66q-9rf6-8795/"}