From 3f96c26322c5106499a7f4eb239721b5a95e0535 Mon Sep 17 00:00:00 2001 From: Zaid Marji Date: Wed, 27 May 2026 15:04:05 +0300 Subject: [PATCH] two_factor/webauthn: gate every entry point with is_webauthn_2fa_supported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is_webauthn_2fa_supported()` was previously checked only at `POST /two-factor/get-webauthn`. The three sibling 2FA WebAuthn HTTP routes — `generate_webauthn_challenge`, `activate_webauthn` (and its `PUT` alias), and `delete_webauthn` — are user-facing Bearer-authed endpoints that any client can hit directly. A client that skips the listing endpoint reached the unguarded `WEBAUTHN.start_passkey_registration` / `WEBAUTHN.finish_passkey_registration` calls. Concretely: under a misconfigured `DOMAIN` (IP literal, hostless URL — anything where `Url::domain()` returns `None`), the `WEBAUTHN` `LazyLock` panics on first access from inside `WebauthnBuilder::new("", ..)`, poisoning the lock so every subsequent WebAuthn touch in the process panics too. The listing endpoint's gate avoids this on the legitimate flow but leaves the same panic-cascade reachable via any direct call to a sibling endpoint. Apply the same guard at the two endpoints that access `WEBAUTHN`: - `generate_webauthn_challenge` (calls `WEBAUTHN.start_passkey_registration`) - `activate_webauthn` (calls `WEBAUTHN.finish_passkey_registration`; also covers `activate_webauthn_put` which is a thin wrapper) `delete_webauthn` doesn't touch `WEBAUTHN` (pure DB work over the `TwoFactor` row), so it doesn't need the guard. The check is a pure function of `CONFIG.domain()`; the response is identical to the listing endpoint's, so well-configured deployments see no behaviour change. --- src/api/core/two_factor/webauthn.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index d8f6feea..f0bd912c 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -130,6 +130,10 @@ async fn get_webauthn(data: Json, headers: Headers, conn: DbC #[post("/two-factor/get-webauthn-challenge", data = "")] async fn generate_webauthn_challenge(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + if !CONFIG.is_webauthn_2fa_supported() { + err!("Configured `DOMAIN` is not compatible with Webauthn") + } + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; @@ -256,6 +260,10 @@ impl From for PublicKeyCredential { #[post("/two-factor/webauthn", data = "")] async fn activate_webauthn(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + if !CONFIG.is_webauthn_2fa_supported() { + err!("Configured `DOMAIN` is not compatible with Webauthn") + } + let data: EnableWebauthnData = data.into_inner(); let mut user = headers.user;