This post documents two cross–site scripting (XSS) vulnerabilities in OpenEMR versions below 8.3.0. A stored XSS vulnerability identified allows an authenticated user administrator or clinician to embed malicious JavaScript code directly into the server’s database. Similarly, an identified vulnerable HTTP GET parameter is reflected directly into OpenEMR without sanitization, allowing a malicious actor to craft a URL that executes arbitrary JavaScript in the browser (i.e., reflected XSS).
Cross-Site Scripting
A cross-site scripting (XSS) vulnerability occurs when an application renders untrusted user input in a web page without proper validation, sanitization, or output encoding. An attacker can exploit this by injecting malicious JavaScript that executes in another user’s browser, potentially allowing session theft, account takeover, phishing, unauthorized actions, or exposure of sensitive data.
- Stored XSS: Occurs when malicious JavaScript is stored by the application, such as in a database, and later rendered to users without proper sanitization or output encoding. Stored XSS is generally more severe because a single malicious payload can persist within the application and execute in the browsers of multiple users who view the affected content, potentially resulting in widespread compromise.
- Reflected XSS: Occurs when attacker-controlled input is included in an application’s HTTP response and rendered without proper sanitization or output encoding, causing malicious JavaScript to execute in the victim’s browser. Unlike stored XSS, reflected XSS typically requires the attacker to induce a victim to submit a specially crafted request, such as by clicking a malicious link, and generally affects users on an individual request basis rather than through a persistent payload.
Stored XSS in Eye Exam Document Notes
Details
// report.php:2552 – safe (text() applied)
echo ‘<td>’ . xlt(‘Date’) . ‘:’ . text(oeFormatShortDate($note->get_date())). ‘</td>’;
// report.php:2555 – UNSAFE (no text())
echo ‘<td.’ . $note->get_note() . ‘<br /><br /></td>’;
executes in the browser session of any clinician or admin who subsequently views the report.
Note::get_note() returns the raw database string. The adjacent date line uses text() for escaping; the note content line does not. This inconsistency is the injection point.
Proof-of-Concept Exploitation
Injection via the Document Note UI
By logging into the application as an admin or clinician and navigating to the retina document’s note form in:
/controller.php?document&view&patient_id=1&doc_id=<DOC_ID>
In the Notes section at the bottom of the document view, enter the following in the textarea:
<img src=x onerror=”alert(document.cookie)”>
Click Add Note. The note is now stored in the notes table via C_Document::note_action_process() → Note::persist() with no sanitization applied to the note field.
Trigger Execution via the Patient Report (Emulating a victim patient or clinician navigating to an eye exam report)
In the browser (logged in as admin or clinician or patient):
- Navigate to Patient → Patient Finder, search for the patient, and select their chart.
- In the left sidebar, go to Reports → Patient Report.
- Check the Eye Exam checkbox for the encounter dated today.
- Click Generate Report.
The browser fires alert(document.cookie) as the report renders, displaying the session cookie of the reviewing clinician.
OpenEMR Remediation
Clearwater disclosed the vulnerability to the OpenEMR team and recommended fixes for the vulnerable code. As of 8/26/2026, OpenEMR 8.3.0 has patched the vulnerable code to protect the application from the stored XSS vulnerability. The full security advisory can be found here:
https://github.com/openemr/openemr/security/advisories/GHSA-xwjf-6q28-9vj7#event-941969
The primary fix is escaping the user-supplied input by using the same mechanism text().
As such:
echo ‘<td>’ . text($note->get_note()) . ‘<br /><br /></td>’;
Reflected XSS in templateHTML HTTP GET Parameter
Details
The templateHtml GET parameter in /portal/import_template.php is reflected directly into the page response without sanitization. An attacker can craft a URL that executes arbitrary JavaScript in the browser of any authenticated Forms Administration user who clicks it.
When a GET request is sent to /portal/import_template.php with a non-empty template Html parameter, the value is passed directly to renderEditorHtml() without any sanitization:
// portal/import_template.php
} elseif (!empty($_GET[‘templateHtml’] ?? null)) {
renderEditorHtml($_REQUEST[‘docid’], $_GET[‘templateHtml’]); // no sanitization
}
No encoding or HTML entity conversion is applied before the value is rendered.
Proof-of-Concept Exploitation
Direct Injection in the HTTP GET Parameter
By issuing the following payload directly to the affected parameter, the application reflects the payload directly in the user’s browser.
/portal/import_template.php?templateHtml=<img src=x onerror=alert(document.cookie)>&docid=0
An attacker may forward this malicious URL directly to a victim user, leading to the exfiltration of the user’s token, and hijack of the session.
OpenEMR Remediation
Similar to the previous XSS vulnerability, OpenEMR 8.3.0 has issued a proper patch to protect issues from this misconfiguration. The security advisory has been published: https://github.com/openemr/openemr/security/advisories/GHSA-rmmr-8498-463g
General Guidance on Preventing XSS
Clearwater Security recommends the following actions to prevent and identify XSS vulnerabilities in your applications:
- Encode output contextually before rendering: Neutralize executable scripts by converting untrusted data into safe, non-executable formats before inserting it into the DOM. Apply encoding appropriate to the output context, such as HTML entity encoding for body text, attribute encoding for HTML attributes, and JavaScript encoding for data inserted into scripts.
- Contextually sanitize HTML inputs: Filter and sanitize user-supplied input using a robust, security-focused library (such as DOMPurify) to remove potentially dangerous tags (e.g., <script>, <iframe>) and attributes (e.g., onload, onerror) when dynamically rendering HTML.
- Establish a security patching process for third-party applications: For off-the-shelf or open-source applications and components, monitor vendor and project security bulletins and implement a defined process to evaluate and apply security patches and updates in a timely manner.
- Integrate security testing into the development lifecycle: For internally developed applications, implement a DevSecOps process that evaluates code for common security vulnerabilities at commit or during the CI/CD pipeline. Where this is not feasible, consider implementing a Static Application Security Testing (SAST) tool to provide automated source code analysis and identify potential vulnerabilities before deployment.
- Implement continuous Dynamic Application Security Testing (DAST): Establish an ongoing DAST program to routinely assess running applications for exploitable vulnerabilities, including XSS, and validate that security controls remain effective as applications change.
- Deploy a Web Application Firewall (WAF): A properly configured WAF can help detect and block common web attacks, including XSS attempts, providing an additional layer of defense. A WAF should supplement, rather than replace, secure application development and remediation practices.
- Conduct regular security testing: Perform routine vulnerability assessments and penetration testing to identify security weaknesses that may not be detected through automated testing and remediate vulnerabilities before they can be exploited.
Read about another vulnerability: OpenEMR SQL Injection identified by Clearwater.



