Browse Source

two_factor/webauthn: gate every entry point with is_webauthn_2fa_supported

`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.
pull/7297/head
Zaid Marji 3 weeks ago
parent
commit
3f96c26322
  1. 8
      src/api/core/two_factor/webauthn.rs

8
src/api/core/two_factor/webauthn.rs

@ -130,6 +130,10 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbC
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, 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<PublicKeyCredentialCopy> for PublicKeyCredential {
#[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn(data: Json<EnableWebauthnData>, 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;

Loading…
Cancel
Save