OpenEMR: SQL Injection in the Patient Portal Document API

Any authenticated portal patient reads the whole database through the Id_BitwiseAnd parameter, including other patients' records and the admin password hashes.

Advisory ID: TP-2026-118
Product: OpenEMR (practice management and electronic health records with a patient portal)
Vulnerability type: SQL injection (CWE-89)
CVE: requested (GitHub CNA), assignment pending
CVSS 3.1: 7.7 (High) · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Affected versions: < 8.5.0
Vendor advisory: GHSA-g2xw-rvhq-x543
Fixed in: 8.5.0
Reported: 13 June 2026

Summary

OpenEMR is open-source electronic health record software with a patient portal through which patients access their own documents. The document endpoint OnsiteDocument.Query copies request parameters onto the Phreeze criteria object without an allowlist, including the operator Id_BitwiseAnd. Its value reaches the SQL without quotes, and the framework's escaping neutralizes quote characters only. Any authenticated portal patient thereby reads the whole database, including other patients' records and the password hashes of every staff and admin account.

Root cause

OnsiteDocumentController::Query() copies every $_REQUEST key whose ucfirst() name matches a criteria property straight onto the object (portal/patient/libs/Controller/OnsiteDocumentController.php:144). The generated criteria class declares a <Field>_BitwiseAnd property for each column (portal/patient/libs/Model/DAO/OnsiteDocumentCriteriaDAO.php), so a parameter named Id_BitwiseAnd is assignable and reaches Criteria::GetWhere(). GetWhere() builds (<column> & <value>) and inserts the value through Escape() with no surrounding quotes (portal/patient/fwk/libs/verysimple/Phreeze/Criteria.php:275). Escape() replaces only \ \0 \n \r \x1a ' " (portal/patient/fwk/libs/verysimple/DB/DataDriver/MySQLi.php:166), so spaces, parentheses and SQL keywords survive, while the sibling operators _Equals and _IsLike wrap their value in quotes. The route api/onsitedocuments carries p_acl => p_all (portal/patient/_app_config.php:168), which makes it reachable for any portal patient, and the injected condition runs outside the per-patient Pid_Equals scope.

Proof of Concept

GET /portal/patient/api/onsitedocuments?Id_BitwiseAnd=0 OR extractvalue(1,concat(0x7e,(SELECT substr(password,1,28) FROM users_secure WHERE username=0x61646d696e)))
Cookie: PortalOpenEMR=<portal-session>

-> {"message":"... XPATH syntax error: '~$2y$12$DmBtGQqVm15ft78eILY9S' ..."}

The value of Id_BitwiseAnd breaks out of the numeric context, and the database error message returns the result of the subquery. The same way, the portal patient reads other patients' names and addresses from patient_data.

Impact

  • Disclosure of every patient's health and demographic records to any portal patient, bypassing the per-patient Pid_Equals scope.
  • Disclosure of the users_secure password hashes of all staff and admin accounts, offline-crackable up to the OpenEMR administrator.
  • Read access to the rest of the openemr schema with the same primitive, including sessions, OAuth and API tokens and the audit log.
  • Triggered by the lowest legitimate role with a single request, no victim interaction.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.