Unauthenticated Booking API Exposes Patient Health Data Across Thousands of Practices
The Risk
Anyone on the internet could pull up real patient details, full name, date of birth, phone, email, and the reason they booked a medical appointment, for thousands of doctors' offices, with no login of any kind. All that was needed was a practice's ID number, which was printed in plain view on each provider's public booking page. The same system also let outsiders change appointment records and trigger administrative actions without signing in. This is the equivalent of leaving the door to a hospital's filing room unlocked and open to the street, for thousands of practices at once.
The Vulnerability
The target was the shared backend that powers online appointment booking for a large network of healthcare practices. A subset of that backend's routes was left completely unauthenticated. This was not a public-by-design API: sibling configuration and administrative routes on the exact same service correctly returned 401 Unauthorized, while the booking routes returned 200 with live data. That split is the fingerprint of a selective authentication-filter misconfiguration, where the server's authentication filter simply does not cover the affected paths.
To read a practice's records, a caller only needed to supply a practice identifier: a random unique id. Critically, that identifier is not a secret. It ships in the public HTML of every provider's booking page, and it is also independently resolvable through an unauthenticated resolver endpoint. So an attacker did not need to guess or brute-force anything; the identifier could be harvested straight off public pages and then handed to the API.
An unauthenticated API-spec endpoint on the same host enumerated the reachable routes, mapping the entire attack surface available to an anonymous caller without any guesswork.
The Attack
The chain was fully scriptable end to end, with no login at any step. A placeholder host booking.example.com is used below in place of the real backend.
Harvesting the Practice Identifier
Each provider's public booking page carries the practice identifier directly in its page source. A single request fetched the page and read the value straight out of the HTML, with no browser automation and no authentication:
curl -s 'https://directory.example.com/doctor/<provider-slug>/bookappointment' \
| grep -oE '"practiceId":"[0-9a-f-]+"' | head -1 An alternate path used an unauthenticated resolver: supplying a provider-level identifier returned the practice identifier plus the affiliate channel it belonged to, confirming the exposed data was the in-scope booking channel's patient data:
curl -s 'https://booking.example.com/booking/provider/locations?providerId=<provider-id>' Reading Patient Records
With the practice identifier in hand, a plain GET returned the practice's patient leads as JSON. No token, cookie, or API key was involved. The response could be read by pasting the URL into a browser address bar:
curl -s 'https://booking.example.com/booking/leads?practiceId=<practice-id>&limit=5' Each record carried a full name, email, phone, preferred appointment dates, and an extra-fields block containing first name, last name, date of birth, and reason for visit. The limit and page parameters were also accepted unauthenticated, so the full per-practice lead history was retrievable, not just the first few records the widget shows by default.
Proving Authentication Is Never Evaluated
To rule out any hidden implicit auth, the read was repeated with a deliberately bogus credential. It still returned 200, proving the authentication layer is not evaluated at all for these routes:
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Authorization: Bearer bogus' \
'https://booking.example.com/booking/leads?practiceId=<practice-id>&limit=5' By contrast, the sibling configuration route on the same host returned 401, confirming the exposure is a selective misconfiguration rather than an intentionally open API:
curl -s -o /dev/null -w '%{http_code}\n' \
'https://booking.example.com/config/business?practiceId=<practice-id>' The Unauthenticated Write Surface
The same service accepted writes without authentication. Probing without touching any real record confirmed this: a PUT to modify a lead's CRM writeback status against a deliberately non-existent lead ID returned a domain-validation error (No such lead, HTTP 400), not 401 or 403. That distinction matters: a domain-validation error means the request passed the security filter and reached business logic with no credentials. A create request behaved the same way:
curl -s -X PUT -H 'Content-Type: application/json' \
-d '{"status":"booked"}' \
'https://booking.example.com/leads/999999999/writeback-status?practiceId=<practice-id>'
curl -s -X POST -H 'Content-Type: application/json' -d '{}' \
'https://booking.example.com/booking/form?practiceId=<practice-id>' Both returned field- or domain-validation errors (HTTP 400), never 401, confirming an anonymous caller could modify appointment records and create booking requests.
The Unauthenticated Administrative Endpoint
The same host also exposed a bulk administrative migration endpoint that executed unauthenticated. A POST returned 200 with a success message ("All businesses were migrated successfully"), with no credentials, confirming an anonymous caller could trigger platform-wide administrative state changes. The sibling administrative and configuration endpoints correctly returned 401, again isolating the failure to a specific set of routes.
The Impact
The exposed data is patient health information: a real name tied to a named doctor, a free-text reason for visit, and a date of birth together are reportable healthcare data. Records were confirmed live across multiple distinct practices, with real consumer email addresses across common providers, matching real phone numbers, and real clinical reasons for the visit.
The scale is platform-wide and demonstrated, not asserted:
- The practice identifier is a random unique id, so it cannot be guessed or enumerated, but it is not a secret. It ships in the public booking-page HTML and is independently resolvable via the unauthenticated resolver, so harvesting is fully scriptable with no login.
- A sample of practices across several specialties and cities returned real patient leads for most of them.
- The public provider directory lists thousands of bookable providers in a single city for a single specialty, and the sitemaps list many thousands of bookable practices discoverable with no login.
- There was no rate limiting: back-to-back unauthenticated reads all succeeded with no throttling or challenge.
The combined outcome is a mass patient-data breach, significant healthcare regulatory exposure, the ability to tamper with appointment records, and a foundation for highly targeted healthcare phishing using a verified patient name plus provider plus contact details plus date of birth.
Remediation
- Apply the authentication filter uniformly across every route on the service. The booking, recall, and administrative migration paths must be gated exactly like the configuration and business paths that already return
401. - Treat the practice identifier as an access-control input only, never as an authorization boundary. Reading a practice's patient records must require an authenticated, authorized session tied to that practice.
- Require authentication and proper authorization on all write and administrative endpoints; remove or lock down the bulk migration endpoint so it cannot be reached anonymously.
- Add rate limiting and anomaly detection on the leads endpoints to prevent bulk harvesting.
- Restrict the unauthenticated API-spec endpoint so the reachable route inventory is not handed to anonymous callers.
- Audit access logs for prior bulk unauthenticated reads and assess breach-notification obligations for the exposed patient data.