All Advisories

OpenReception

Unauthenticated WebAuthn Passkey Injection

POST /api/auth/passkeys adds a WebAuthn credential to the account named by a request-body userId, with no authenticated session and no WebAuthn registration ceremony. An attacker adds a passkey they control to a confirmed staff account, then logs in as that account. The account's userId is not secret: the public booking flow's staff directory returns it to any unauthenticated caller. The companion register/[id] endpoint was hardened against passkey injection in CVE-2026-48087; that change does not apply to this endpoint. All releases through 1.1.0 are affected; the issue is fixed in 1.1.1.

Authored byVolker Schönefeld, Simon Weber2026-06-04
SeverityCriticalCVSS 9.8CVSS 3.1 VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HCWECWE-306 (Missing Authentication for Critical Function)ProductOpenReceptionAffected VersionsAll releases through 1.1.0.Fixed In1.1.1CVEPendingGHSAGHSA-g233-m625-m3pc

Description

OpenReception is self-hosted, per-tenant appointment booking for medical practices, with end-to-end encryption between clients and staff. Staff and tenant administrators authenticate with WebAuthn passkeys. We appreciate the project's work on a privacy-preserving booking design and the maintainers' prompt fix.

The endpoint that adds a passkey to an account, POST /api/auth/passkeys, requires no authenticated session and runs no WebAuthn registration ceremony. The handler reads a userId and a public key from the request body and stores them:

src/routes/api/auth/passkeys/+server.ts:98-129

export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.userId || !body.passkey) { /* 400 */ }
if (!body.passkey.id || !body.passkey.publicKey) { /* 400 */ }
const counter = WebAuthnService.extractCounterFromCredential(body.passkey);
await UserService.addAdditionalPasskey(body.userId, {
id: body.passkey.id,
userId: body.userId,
publicKey: body.passkey.publicKey,
counter,
deviceName: body.passkey.deviceName || "Unknown Device",
});
};

View source →

The handler never reads locals.user and never verifies a server-issued challenge, an attestation, an origin, or an RP ID. UserService.addAdditionalPasskey stores the supplied public key on the target account; the only gate is that the account is not still in the INVITED state, so any confirmed account is accepted.

The request never reaches an authentication guard. The API middleware rejects unauthenticated requests only for paths under /api/admin; every other /api/* route is expected to check the session in its own handler, and this one performs no check. The endpoint's OpenAPI annotation marks it as requiring authentication. The handler contains no session check.

At login, POST /api/auth/login verifies the WebAuthn assertion against the public key stored for the account and issues a session. Because the stored key is the attacker's, the attacker's assertion verifies and a full session is issued for the victim account.

The target userId is not secret. GET /api/tenants/[id]/appointments/staff-public-keys returns { userId, publicKey } for every staff member with active crypto, gated only by a booking access token that any anonymous visitor mints through the public booking bootstrap (a proof-of-work challenge whose token is returned in the response). An attacker who knows a staff member's email address obtains that member's userId, injects a passkey, and logs in.

The companion bootstrap endpoint POST /api/auth/register/[id] was hardened against passkey injection in CVE-2026-48087. That change does not apply to /api/auth/passkeys, which retains the original injection primitive; this advisory covers the remaining endpoint.

The unauthenticated path takes over a tenant STAFF account. From a hijacked STAFF session, the tenant staff listing exposes TENANT_ADMIN accounts with their userId and email, so the same injection escalates to tenant administrator. A GLOBAL_ADMIN has no tenant association and appears in no tenant-scoped listing, so it is not reachable through this chain.

A hijacked session does not by itself decrypt client data. OpenReception derives each staff member's decryption key from their authenticator during login and binds the stored key shares to the credential that registered them, so the injected passkey produces an authenticated session but not the key material that protects the encrypted appointment payload. The exposure is therefore the tenant's metadata, staff personal data, and the integrity and availability of every appointment record, not the encrypted client payload.

Impact

  • An unauthenticated remote attacker who knows a staff member's email address takes over that staff account and holds a session with its authority over the tenant. The end-to-end-encrypted appointment payload (the client's name, contact details, and reason for the appointment) stays encrypted: it is unlocked only by a key derived from the staff member's authenticator at login, which the injected passkey cannot reproduce. The session does expose the tenant's appointment calendar metadata (dates, times, channel, agent, status), the full staff roster (names, emails, roles, last-login times), and stored client email hashes together with a lookup that confirms whether a given email address belongs to a client of the practice.
  • Within that session the attacker acts with the account's privileges. A STAFF session can cancel, confirm, deny, forge, or permanently delete appointments and can issue client PIN-reset tokens that take over client accounts. The takeover also escalates within the tenant: a STAFF session enumerates TENANT_ADMIN accounts and takes them over the same way, and a TENANT_ADMIN can invite further administrators, modify or remove staff, and rewrite tenant configuration and channels.
  • Appointments are hard-deleted with no recovery. Removing a staff member also removes their tunnel key shares, so the encrypted appointment data those shares protected becomes permanently undecryptable to the practice, which is data destruction rather than downtime. The people affected are the practice's staff, whose accounts are taken over, and its clients, whose appointment metadata is exposed and whose records can be altered or destroyed; the encrypted appointment payload itself remains protected by a per-credential key the attacker cannot derive.

Mitigation

Upgrade to OpenReception 1.1.1. The fix requires an authenticated session on POST /api/auth/passkeys, binds the new credential to the logged-in user, and verifies a full WebAuthn registration ceremony (server challenge, expected origin, and RP ID) before storing it, matching the protections the register/[id] flow already applies. Operators who ran an affected version can review the userPasskey table for credentials added without a preceding registration challenge and re-register affected accounts. After the operator upgrades, staff should re-register their own passkeys and check their account for any passkey or active session they do not recognize.

References

How We Can Help

Who We Are

The security researchers behind this advisory.

Dr. Simon Weber Profile

Dr. rer. nat. Simon Weber

Senior Pentester & MedSec Researcher

I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.

  • PhD on Hospital Cybersecurity
  • Critical vulnerabilities found in hospital systems
  • Alumni of THB MedSec Research Group
  • gematik Security Hero
Volker Schönefeld Profile

Dipl.-Inf. Volker Schönefeld

Senior Application Security Expert

As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.

  • 20+ years as CTO, 50M+ app downloads
  • Architected and secured large-scale IoT fleets
  • Certified Web Exploitation Specialist
  • gematik Security Hero

Looking for a Penetration Test?

Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.