Table of Contents OpenReception is an open-source appointment booking platform aimed at medical practices, with a self-hosted edition and a commercially hosted multi-tenant version. Its 1.0 release was covered as an open-source alternative to commercial schedulers like Doctolib by heise and Golem . Its selling point is end-to-end encryption: patient data is supposed to stay readable only to the staff it is meant for, while the server holds ciphertext it cannot decrypt. A single instance can host many independent practices, and it ships as a SvelteKit application backed by PostgreSQL. I audited the released code shortly after the 1.0 line shipped and ran a local instance from the official docker-compose to validate findings. Over the audit I reported 16 vulnerabilities. All of them were assigned CVEs, four were rated Critical, and all were fixed before public disclosure on 2026-05-20. Two of the four hand an attacker full administrative control, one lets them log in as anyone, and the last quietly defeats the encryption the whole product is built on. Tenant admin self-promotion to GLOBAL_ADMIN (CVE-2026-48086) # OpenReception’s two administrator tiers map onto its multi-tenancy: a TENANT_ADMIN runs a single practice, while a GLOBAL_ADMIN runs the whole platform and can create tenants, list every practice, and manage other administrators. A tenant admin edits their own staff through PUT /api/tenants/{tenantId}/staff/{staffId} , and a staff record carries a role. The handler authorizes the caller with checkPermission(locals, tenantId, true) , which confirms they are an admin of that tenant, and then writes whatever role the request body contains. The body is validated against a Zod schema, and that schema’s role enum includes GLOBAL_ADMIN . Nothing checks that the requested role is not above the caller’s own, so the schema validation ends up being the entire authorization decision, and it accepts GLOBAL_ADMIN . A tenant admin promotes themselves with one request: PUT /api/tenants/02fa8204-.../staff/<own-user-id> HTTP/2 Host: clinica.localtest.me Cookie: access_token=<TENANT_ADMIN session> {"role":"GLOBAL_ADMIN"} The response is 200 with the new role. The session token they are holding was issued before the change and still says TENANT_ADMIN, so they log in again and the fresh token carries GLOBAL_ADMIN. An admin-only endpoint that returned 401 a moment earlier now returns 201, and they can create tenants and read every practice on the instance. The same handler accepts any staffId in the same tenant, so an attacker can promote a second account instead of their own and leave their own role history clean. Unauthenticated GLOBAL_ADMIN creation after setup (CVE-2026-48085) # When an instance is first deployed, the operator claims it by creating the initial GLOBAL_ADMIN through the setup page at /setup/create-admin-account . A guard is meant to retire that page once an administrator exists. The guard lives in the setup layout’s load function, and a load function runs only on a GET. The form action that handles the POST, the one that actually creates the account, never rechecks whether an admin already exists. So on a fully claimed and configured instance, an unauthenticated POST to the setup action creates another GLOBAL_ADMIN. The account is written with is_active = true and confirmation_state = ACCESS_GRANTED , and the row is committed before the confirmation email is sent, so it exists even if the SMTP step is slow or the connection drops. SvelteKit’s built-in same-origin check rejects a blind browser CSRF, but any client that sets a matching Origin header sends the request: POST /setup/create-admin-account HTTP/2 Host: admin.localtest.me Content-Type: application/x-www-form-urlencoded Origin: https://admin.localtest.me type=passphrase&email=attacker@evil.test&language=en&passphrase=<long passphrase> A login with that email then returns a session whose role is GLOBAL_ADMIN, and GET /api/tenants returns the full list. The fix adds an adminExists() check inside the action, not just the layout. WebAuthn passkey injection (CVE-2026-48087) # Passkey registration goes through POST /api/auth/register/{userId} , and the flow leans on two cookies set during the preceding challenge step: the WebAuthn challenge itself and a registration email. The registration handler checks two things: that body.email equals the registration-email cookie, and that the WebAuthn response validates against the challenge cookie. It never checks that the userId in the URL belongs to that email. The credential is then written to whatever user the URL names: // register/[id]/+server.ts, simplified // body.email === cookie email -> checked // WebAuthn response matches the challenge -> checked await UserService.addPasskey(params.id, credential); // params.id is never tied to the email Because those email checks only ever compare the attacker’s own values, the attacker drives the whole flow with their own email and authenticator: Call POST /api/aut...
A critical audit of OpenReception's 1.0 release line revealed 16 vulnerabilities, including unauthenticated GLOBAL_ADMIN creation (CVE-2026-48085) and tenant admin self-promotion (CVE-2026-48086), which allow full administrative control, as well as an end-to-end encryption bypass. All reported vulnerabilities were assigned CVEs, with four rated Critical, and were fixed prior to public disclosure on 2026-05-20. IT professionals running self-hosted instances should immediately update to the patched version released on that date.