See how openreception compares to other vendors in security performance
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to 1.1.1, POST /api/auth/passkeys accepts a request-body userId and attacker-supplied passkey without an authenticated session, does not call WebAuthnService.verifyRegistration, and does not bind enrollment to locals.user.id. An unauthenticated attacker who knows the public tenant ID and the target staff email can use the public booking bootstrap and GET /api/tenants/[id]/appointments/staff-public-keys to obtain candidate userId values. The attacker first causes UserService.addAdditionalPasskey to store a controlled public key for a candidate userId, then attempts login with the target email; the login check compares verificationResult.userId with the email-resolved account and reveals whether the injected credential belongs to that target. Repeating this injection-before-login sequence identifies the matching userId, and the normal login endpoint accepts the attacker's assertion for the stored key and creates a STAFF session. The session can expose tenant data and reveal TENANTADMIN identifiers for further takeover; GLOBALADMIN accounts are not reachable through this tenant-scoped path. A hijacked TENANTADMIN can modify or delete tenant resources and key shares, potentially making appointment data permanently undecryptable and taking booking services offline. This issue is fixed in version 1.1.1.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.6, when a TENANTADMIN deletes an existing staff user, the underlying StaffService.deleteStaffMember() runs an additional invite cleanup that deletes from the central userinvite table by email. The email clause has no tenantId predicate. Any pending invite in any tenant that shares the deleted staff's email is removed. A TENANTADMIN of tenant A who deletes a staff record with email victim[@]example[.]com also deletes the pending invite for victim[@]example[.]com in tenant B, even though they have no relationship to tenant B. The user-side delete is correctly scoped (eq(user.id, staffId), eq(user.tenantId, tenantId)), and the pending-invite-only delete path (when staffId is itself an invite ID) is also tenant-scoped. The bug is specifically in the invite cleanup that runs as a side effect of deleting an existing staff user. Version 1.0.6 patches the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.1.0, the GET handler at /api/tenants/{id}/appointments/{appointmentId} performs no authorization check before returning the appointment record. Any party who knows or obtains a valid appointment UUID receives the full row, including channel and agent IDs, time and timezone, status, and the AES-GCM ciphertext components (encryptedPayload, iv, authTag, dataKey). The same file's DELETE handler calls checkPermission(locals, tenantId, true) before allowing deletion. The intent is clear: appointment records are tenant-scoped and require authentication to access. The GET handler is missing the equivalent call. The middleware chain (apiAuthHandle, authGuard) does not compensate: API paths bypass authGuard entirely, and apiAuthHandle does not block requests to non-admin paths when no token is present. Version 1.1.0 patches the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.2, a TENANTADMIN promotes themselves to platform-wide GLOBALADMIN through a single PUT request. The role-update handler accepts the GLOBALADMIN enum value from any tenant admin updating their own tenant's staff. No policy check enforces that "only an existing GLOBALADMIN may grant GLOBALADMIN", so the schema validation IS the authorization decision. After re-login, the JWT contains the new role and the formerly-tenant-scoped admin reaches every other tenant on the platform. On the hosted OpenReception service this is a scope-changed escalation: a single customer-side tenant administrator gains full platform-wide administrative control over all other tenants' configuration, users, staff records, operational metadata, and tenant lifecycle. Plaintext appointment contents remain subject to the E2E model unless chained with the staff-crypto poisoning issue (V-4) or with staff-passkey hijacking (V-1). On a single-tenant self-hosted deployment it is still a privilege escalation because TENANTADMIN should not be able to create new tenants, modify global configuration, or manage other administrators. The same handler also accepts updates targeted at any colleague within the tenant. A tenant admin can promote a separate collaborator account instead of themselves, leaving their own audit trail clean while the platform-wide breach happens through a separate identity. Version 1.0.2 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.4, the PIN-type challenge throttle uses emailHash as the only key. The throttle rows live in the central challengethrottle table, which is shared across all tenants. Every tenant's /api/tenants/{id}/appointments/verify-challenge endpoint increments the same row when a PIN response fails, and every tenant's /api/tenants/{id}/appointments/challenge endpoint reads the same row when deciding whether to issue a new challenge. When the same emailHash exists in multiple tenants on the same OpenReception instance (the same patient holding tunnels in two different clinics that share the platform), an attacker who knows the patient's email can lock out that patient on tenant B by issuing failed challenge responses against tenant A. The attacker needs no relationship to tenant B; the lockout propagates through the shared throttle row. The lockout escalates with repeated failures. The first lockout triggers at 4 failed attempts and lasts approximately 60 seconds. Subsequent failures escalate the lockout duration to 5 minutes, 30 minutes, and 60 minutes per the throttle service's escalation logic. Repeated bursts produce sustained denial of service against the targeted email. Version 1.0.4 patches the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.2, a TENANTADMIN can store javascript: URLs in the tenant links configuration (website, imprint, privacyStatement). These values are returned to the patient-facing landing page via /api/public, hydrated into the SvelteKit Button component, and rendered as <a href="javascript:..."> elements without URL-scheme filtering. A patient who clicks any such link executes the attacker's JavaScript inside the patient browser origin, where patient form data is read before client-side encryption is applied. This breaks the project's central trust claim that the server is an untrusted relay and that administrators cannot read patient data. Patient-side encryption happens after form input, so JavaScript executing in the patient origin can read or alter the plaintext before encryption is performed. Version 1.0.2 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.1, a fully provisioned OpenReception instance accepts unauthenticated POST requests to /setup/create-admin-account and creates additional GLOBALADMIN accounts without verifying that an admin already exists. Any unauthenticated network attacker who can submit a same-origin form POST gains full platform-level administrative control. The newly created account is isactive=true with confirmationstate=ACCESSGRANTED and does not require completing email confirmation; the GLOBALADMIN row is created active and immediately usable. Login and tenant enumeration succeed without any further interaction. This is distinct from the deployment race condition already documented on the Claiming an instance page. That documented race covers the window between deployment and first claim. The bug reported here works after the operator has properly claimed and configured the instance: the layout-level guard that protects the setup page only redirects on GET, while the default form action handler creates the user without rechecking adminExists(). Three GLOBALADMIN accounts were created in succession during testing, with no rate limiting observed. Audit-specific event logging beyond standard application logs was not assessed; the standard [error] line that surfaces only when a uniqueness conflict is hit is not the same as a security event for "additional admin created post-claim". The form post is rejected for browser drive-by CSRF by SvelteKit's built-in same-origin check, but any tool that supplies a matching Origin header (curl, Burp, automated scanners, server-side proxies) bypasses this trivially. No additional preconditions exist. Users should upgrade to version 1.0.1 to receive a patch.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. The new-client booking flow in versions 1.0.1 and prior consists of three calls: bootstrap-challenge (returns a 16-bit PoW challenge with difficulty=4 leading hex zeroes), bootstrap-verify (validates the PoW and issues a Bearer booking access token), and create-new-client (consumes the token and creates the tunnel and first appointment). The token correctly binds to tenantId, tunnelId, clientPublicKey, and emailHash, but never to channelId. The bootstrap-challenge request schema does not even accept a channelId, and the issued token's payload contains no channel information. Independently, the service function createNewClientWithAppointment checks only channel.archived = false. The channel.isPublic check that protects addAppointmentToTunnel is missing in the new-client path. The combination means: an attacker completes the bootstrap flow normally (16-bit PoW, completes in well under one second on commodity hardware, no rate limiting beyond the throttle store), receives a valid booking access token, and then submits the create-new-client payload with channelId pointing to a private (isPublic = false) channel. The booking lands as CONFIRMED if the target channel has requiresConfirmation = false (the default), otherwise as NEW. The patient-facing UI does not list private channels in its picker (/api/public/channels correctly filters isPublic = true), so the channel ID must be obtained out of band. The companion finding V-10 (schedule endpoint discloses private channels) provides exactly that: a single unauthenticated GET reveals every private channel ID for any tenant. V-10 plus V-11 together make private channels fully reachable to anonymous attackers. As of time of publication, no known patched versions are available.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.5, the add-to-tunnel endpoint creates a new appointment row in any client tunnel without any caller authentication. A request that supplies any valid tunnelId and any valid emailHash (the two need not belong to the same tunnel) results in an inserted appointment with status = "CONFIRMED", attacker-controlled ciphertext fields, attacker-controlled date and duration, and an attacker-chosen agent. The endpoint validates only that some tunnel exists with the given emailHash, then writes the appointment using the attacker-supplied tunnelId directly. The emailHash lookup is effectively an existence check on the tenant; it does not authenticate the caller as the owner of the supplied tunnelId. Combined with the absence of any session, Authorization header, booking access token, or PoW, this makes the endpoint accept arbitrary appointment writes into arbitrary tunnels. By contrast, the sibling endpoint create-new-client (used to bootstrap a brand-new client tunnel) requires a Bearer bootstrap booking access token issued by the bootstrap-challenge / bootstrap-verify flow. The add-to-tunnel endpoint, intended for return-clients booking additional appointments, has no equivalent gate. The application's own middleware confirms this is intentional: add-to-tunnel is explicitly listed in the apiAuthHandle public-route allowlist alongside the bootstrap and challenge endpoints (which legitimately have no session). Version 1.0.5 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.2, the /api/log endpoint accepts unauthenticated POST requests, applies no schema validation to the message body, writes attacker-controlled content directly into the application's stdout log, interprets newline characters as real line breaks, and enforces no size or rate limits. Three independent abuse modes follow: log injection (forge log lines that look like legitimate system events), log volume DoS (saturate the logging pipeline at sustained 100+ requests per second of small messages), and oversized-payload submission (100 KB payloads accepted; larger sizes not tested). The most operationally damaging mode is log injection. An attacker can inject lines that an operator scanning logs would mistake for real system errors, mask their own activity behind fake noise, or pollute SIEM alerting rules with crafted false positives. A line such as [error]: injected admin error injected from an unauthenticated source is indistinguishable from the application's own error output once written to disk. Version 1.0.2 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.6, the bootstrap challenge endpoint at /api/tenants/{id}/appointments/bootstrap-challenge issues a SHA-256 proof-of-work with difficulty=4 hex zeros, equivalent to 16 bits of work. Modern hardware solves this in under 200 milliseconds, providing essentially no friction against automated abuse of the patient booking flow. Proof-of-work is used in the booking flow as a rate-limiter for unauthenticated clients establishing tunnels and submitting appointments. At 16 bits of difficulty, the construct is decorative rather than effective. An attacker can solve PoW challenges as fast as the server can issue them, defeating the rate-limiting purpose. The handler also calls challengeThrottleService.checkThrottle(binding, "passkey"), but the binding includes attacker-controlled values (tunnelId, clientPublicKey, and optional emailHash). For each fresh attempt, the attacker can supply new values, producing a new throttle key and bypassing the per-binding accumulation. Practical abuse friction is therefore the PoW difficulty itself, not a stable per-IP or per-email server-side throttle. Version 1.0.6 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.2, when a user navigates to the /logout page, the page's server-side load handler deletes the accesstoken cookie before calling /api/auth/logout via an internal event.fetch(). The internal fetch consequently runs without the auth cookie, so apiAuthHandle rejects it, the logout handler never executes, and SessionService.revokeSession() is never called for the current session. The DB session row remains valid until its natural expiry (one week by default). The user sees a successful logout (cookie gone, UI returns to login), but any party still holding a copy of the now-deleted access token can continue making authenticated API calls until the session naturally expires. The root cause is a simple ordering mistake. The same auth subsystem implements the correct order in /api/auth/logout: revoke the current DB session first, then delete the cookie. The page-level wrapper does the opposite. Version 1.0.2 initiates server-side logout before removing authentication cookies and first appears in version 1.0.2. Version 2.0.0 later replaces this with a race-free client-side logout flow.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.4, the route POST /api/tenants/{tenantId}/staff/{staffId}/crypto accepts and stores attacker-controlled ML-KEM-768 public keys against any tenant on the platform without authentication. The handler logs an "Unauthorized crypto key storage attempt" warning when neither a session nor a registration cookie is present, then proceeds to insert the row regardless. The platform's E2E claim that "even administrators cannot view sensitive information" is broken: any unauthenticated network attacker can register themselves as an additional encryption recipient for any tenant's future patient appointments. A second variant of the bug suppresses the unauthorized-warning log entry. The Zod schema makes the email field optional. When the request body omits email and the request carries no registration cookie, the comparison registrationEmail === email becomes undefined === undefined, which evaluates to true. The handler treats the request as a legitimate registration flow, skips the warning entirely, and stores the row. Successful storage is still recorded as an [info] log line, but the security-relevant warning that operators are most likely to monitor or alert on is gone. The staffcrypto table has no unique constraint on userid, so an arbitrary number of attacker rows can coexist for the same staff identifier and all return as isactive=true. The supplied staffId does not need to match any existing user or pending invite. Schema validation on passkeyId, publicKey, and privateKeyShare is also weak: the literal string <placeholder-base64> was accepted, indicating no length, format, or cryptographic-validity check beyond field presence. This weakness is independent of the auth bypass but compounds it: a poisoned directory can also be filled with malformed entries that break legitimate booking flows. The injected key is consumed by the public booking flow. After completing the unauthenticated bootstrap-challenge and bootstrap-verify ceremony as a "patient", the resulting bookingAccessToken is accepted by GET /api/tenants/{id}/appointments/staff-public-keys, which returns the attacker-controlled keys alongside any legitimate ones. A new appointment encrypts its tunnel key with ML-KEM to all listed recipients, so the attacker becomes a co-recipient of the encryption and can decapsulate the tunnel key with the matching secret. From there, all appointment payloads for that booking are decryptable. Version 1.0.4 patches the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.2, the registration handler at POST /api/auth/register/{userId} validates the relationship between the WebAuthn challenge and the registration cookie's email but never validates that the userId in the URL belongs to that email. An unauthenticated attacker requests a challenge for their own email, generates a registration response with their own authenticator, and submits it against any victim user's URL. The challenge-vs-cookie email match passes, the WebAuthn ceremony validates, and addPasskey writes the attacker's credential into the victim's userpasskey rows. The next victim-email login accepts a passkey assertion from the attacker's authenticator and issues a session as the victim. User IDs are not strictly secret on this platform, but the exact set of exposure surfaces should be assessed by the maintainers. Staff-list endpoints return user IDs to authenticated tenant members per the route signature; live verification of all exposure surfaces (whether user IDs leak through any unauthenticated route, through invite-confirmation URLs, or through other administrative views) is part of the pending live PoC. Where the attacker knows the victim's email and userId, the analysis below becomes account takeover. Version 1.0.2 fixes the issue.
OpenReception's appointment booking software provides an end-to-end encrypted appointment booking platform. Prior to version 1.0.5, the unauthenticated /api/tenants/{id}/schedule endpoint returns every non-archived channel for a tenant regardless of the channel's isPublic flag. Channels marked isPublic = false are intended to be invisible to public callers; the dashboard creates them deliberately to hide internal-only services from the patient booking UI. The schedule endpoint ignores the flag entirely and discloses channel names, descriptions, IDs, agent associations, pause status, confirmation requirements, and computed slot availability for the requested date range. The asymmetry between addAppointmentToTunnel (which enforces eq(channel.isPublic, true)) and the schedule endpoint (which does not) confirms the design intent: private channels exist as a real access boundary in the booking flow, just not in the schedule disclosure. Version 1.0.5 patches the issue.