Apex, Cantina’s autonomous OffSec agent, found OpenEMR access-control vulnerabilities that allowed a logged-in patient to retrieve another patient’s consent document and a low-privilege staff user to recategorize and download a protected patient file. The maintainers fixed both findings in OpenEMR 8.3.0.
TL;DR
- Nine published findings: Apex found two High and seven Medium vulnerabilities that exposed failures in patient isolation, document access, prescription permissions and payment handling.
- Two High findings, fixed in 8.3.0: empty request fields removed the portal’s patient filter while a document-category change persisted even after the server returned a denial, with both advisories published on August 18, 2026.
- Seven Medium findings, fixed in 8.4.0: the maintainers published these advisories and released OpenEMR 8.4.0 on September 13, 2026.
- What the tests showed: the two High proofs retrieved seeded patient documents using authenticated accounts without establishing a production-wide compromise or a count of real patients affected.
The two High findings show what happens when a system knows who you are but fails to enforce which patient records you may access, so we explain those failures and their fixes before covering the seven additional published findings.
What OpenEMR does
OpenEMR is an open-source electronic health records and medical practice management application that clinics use to manage patient information, appointments, documents, prescriptions, billing and patient-portal interactions.
An API lets another program request data or perform an action while OAuth lets a connected application act with approved permissions. SMART on FHIR adds healthcare-specific conventions for connecting an app to medical records using FHIR (pronounced “fire”), a standard for exchanging healthcare data.
A scope describes what an app may do through a name such as user/appointment.read, where user identifies the access context and appointment identifies the resource while read identifies the permitted actions. OpenEMR must check both the resource and the actions before allowing access.
Patient isolation is a separate requirement because permission to read a type of record does not establish whose records the app may read.
Findings examined in detail
| Finding | Severity | Demonstrated result |
|---|---|---|
| Empty patient-portal filters | High | An authenticated patient retrieved another patient’s locked consent artifact. |
| Classic document-controller authorization | High | A low-privilege staff account recategorized and downloaded a protected document despite a denial response. |
High: Empty fields removed the portal’s patient filter
The patient-portal advisory demonstrates a concrete disclosure: an authenticated patient retrieved another patient’s locked consent artifact.
What the code was supposed to do
OnsiteDocumentController builds the search for portal documents by setting Pid_Equals to the patient identifier from the authenticated session and using DenialReason_IsNotLike to exclude documents marked Locked.
In plain English, the query means to show this patient’s documents while leaving locked artifacts out.
The request-binding loop then copies matching request fields into the same criteria object, which holds the filters used to build the database query. This loop did not distinguish a user’s search preferences from the server’s patient-access restrictions.
How two empty values changed the result
- The patient logged into the portal with valid credentials.
- The request supplied empty values for
pid_EqualsanddenialReason_IsNotLike. RequestUtil::Get(), which reads a request parameter, returned those empty values to the binding loop.- The loop overwrote the patient identifier and the locked-document restriction.
- The query builder skipped both empty filters.
An empty patient filter therefore meant “do not filter by patient” rather than “return no records,” while omitting the page parameter selected the branch returning all rows that matched the remaining criteria.
The test used attacker patient 4 and one locked artifact belonging to patient 5, with the ordinary request returning one row for patient 4. The changed request returned two rows covering patients 4 and 5 and included the victim artifact’s document body, template data, filename, path and stored signature-related fields.
The proof established disclosure of that seeded artifact without demonstrating signature creation, document modification, a complete-chart export or a production-wide record count.
How the fix works
The advisory lists versions before 8.3.0 as affected and 8.3.0 as patched after OpenEMR added an upstream check through PortalSessionPidGuard::assertRequestKeysMatchSession().
That function checks whether patient-identifier fields in the request agree with the logged-in patient and rejects empty, invalid or mismatched values before the controller runs. The portal guard patch includes criteria-style keys such as Pid_Equals and addresses the demonstrated request at entry.
Keeping patient authorization separate from editable search criteria remains useful defense in depth, although the continued presence of a generic binding loop does not establish that this exploit works against the patched release.
High: A denied request still changed who could read a document
The classic document-controller advisory used a low-privilege staff account that could access patient demographics through patients|demo but lacked the patients|docs document permission.
The target was seeded document 4242 belonging to patient 3102 and its original Medical Record category required the document permission that the account did not have.
What the functions do
checkControllerAcl() applies the starting permission requirement for a controller but did not impose a document permission because the document controller was missing from its lookup table, CONTROLLER_ACL_MAP.
move_action_process() changes a document’s category and can therefore change who may read the file when that category determines its protection.
retrieve_action() sends the document’s bytes to the caller using its current category, but the vulnerable path checked the document’s patient only when a patient identifier was present.
The move happened before the denial
- The staff account requested a move of document 4242 to category 5, Patient ID card.
move_action_process()changed the category mapping without first checking the source document’s ownership and write authorization.- A later step returned
403 Forbidden. - The category change remained in the database.
The response said no after the state had changed and document 4242 now sat in a category readable with the staff account’s demographics permission.
The attacker then requested the file with an empty patient_id that the dispatcher converted to null, removing the ownership check because retrieve_action() compared patient ownership only when the value was non-null.
A wrong non-empty patient identifier returned 403 while an empty identifier returned 200 and the seeded victim document’s bytes.
This proof established persistent re-categorization and retrieval of one known document without demonstrating document enumeration, changes to the file’s contents or the optional branch that reassigns a document to another patient.
How the fix works
The advisory lists versions before 8.3.0 as affected and 8.3.0 as patched. The classic document-controller patch added the document controller to the base permission map and checked the source document before mutation.
The patch also makes external retrieval reject missing or invalid patient context while keeping an internal pre-validated retrieval path available through returnRetrieveKey.
The original document and proposed change must be authorized before committing the move because a later denial cannot undo a database update unless the application explicitly rolls it back.
Additional findings
Seven additional Medium findings discovered by Apex were published on September 13, 2026, with each advisory listing versions before 8.4.0 as affected and 8.4.0 as patched:
- REST document downloads: a user with document upload and download access could make the server return a local file, including its database configuration. Advisory: GHSA-xgxv-36g2-hq5g.
- APICSRFTOKEN session setup: a low-privilege staff session with a valid session token could read questionnaire answers belonging to multiple patients. Advisory: GHSA-5xf2-3h9x-f4m6.
- Rainforest portal payments: an authenticated portal patient could use browser-controlled payment metadata to produce unauthorized billing credits on another patient’s account. Advisory: GHSA-525m-fch5-vqqm.
- SMART patient selection: a user with demographics access could select another patient during authorization and retrieve that patient’s clinical note and document. Advisory: GHSA-rm4j-gxwp-qh48.
- Prescription APIs: a staff user with read-only Medical/History access could list prescriptions, insert a prescription, and deactivate an existing prescription for another patient. Advisory: GHSA-qrc2-5wvq-f68q.
- Portal onsite-document APIs: a low-privilege core staff session could list, read, and modify other patients’ portal documents. Advisory: GHSA-4r76-rq83-hgpg.
- FHIR chart exports: a staff user with demographics and document permissions could use
$docrefto generate and download another patient’s C-CDA clinical summary. Advisory: GHSA-r79c-4hf6-fgp3.
Affected versions and fixes
| Findings | Affected versions | First patched release |
|---|---|---|
| Two High findings: patient-portal filters and classic document-controller authorization | Before 8.3.0 | 8.3.0, released August 18, 2026 |
| Seven Medium findings listed above | Before 8.4.0 | 8.4.0, released September 13, 2026 |
All nine published findings covered here have a released fix and the 8.4.0 release notes explicitly list the seven Medium advisories.
Source and commit-ancestry review confirmed that both High document fixes were included in the 8.3.0 tag. The patch status above reflects the public advisories and release notes; this review did not rerun the proofs against the packaged releases.
Disclosure timeline
- April 30, 2026: OpenEMR committed the acdd996 snapshot used by the two High document proofs.
- August 11, 2026: OpenEMR merged the classic document-controller remediation.
- August 12, 2026: OpenEMR merged the portal patient-identifier guard.
- August 18, 2026: OpenEMR released 8.3.0 and published the two High advisories.
- September 13, 2026: OpenEMR released 8.4.0 and published the seven Medium advisories listed above.
Engineering lessons
Keep patient scope outside editable search filters
The portal established the correct patient scope before a generic request binder overwrote it, showing why identity and ownership restrictions should remain separate from fields a user can supply to narrow a search.
Treat policy changes as privileged writes
Moving a document into a different category can change who may read it, so authorize the source object and destination before changing the mapping and inspect persistent state when testing denied requests.
Match permissions to the action and the patient
A valid staff session or permission to read medical history is not sufficient authorization to change a prescription, so check the specific action and the target patient’s access restrictions before running it. The prescription API advisory demonstrates why that distinction matters.
Frequently asked questions
Did the original patient-document findings require authentication?
Both proofs required authentication, with the portal proof using a logged-in patient account and the classic document-controller proof using a staff account with demographics access but without the required document permission.
Were the original patient-document findings fixed?
Both public advisories list 8.3.0 as patched and source review confirmed that the relevant remediation commits were included in that release, although the proofs were not rerun against the packaged release.
Are the additional findings fixed in 8.4.0?
All seven published Medium advisories list versions before 8.4.0 as affected and 8.4.0 as patched, with OpenEMR releasing 8.4.0 on September 13, 2026 and listing all seven advisories in its security fixes.
Test your application with Apex
Apex, Cantina’s autonomous OffSec agent, tests how access controls behave across real application workflows.
Sources
- Patient-portal filter advisory: GHSA-v688-m84c-5h4v
- Classic document-controller advisory: GHSA-89j2-7cg2-mm93
- REST document download advisory: GHSA-xgxv-36g2-hq5g
- APICSRFTOKEN session setup advisory: GHSA-5xf2-3h9x-f4m6
- Rainforest portal payment advisory: GHSA-525m-fch5-vqqm
- SMART patient-selection advisory: GHSA-rm4j-gxwp-qh48
- Prescription API advisory: GHSA-qrc2-5wvq-f68q
- Portal onsite-document API advisory: GHSA-4r76-rq83-hgpg
- FHIR chart-export advisory: GHSA-r79c-4hf6-fgp3
- OpenEMR 8.3.0 release
- OpenEMR 8.4.0 release
- Portal guard remediation
- Classic document-controller remediation