CVE-2026-24738

Passport to Purgatory: Infinite Loops in gmrtd

Alon Barad
Alon Barad
Software Engineer

Jan 27, 2026·6 min read·4 visits

Executive Summary (TL;DR)

The `gmrtd` library, used to read ePassports, blindly trusts the length declared by the NFC chip. By presenting a chip that claims to hold 4GB of data, an attacker can force the reader into an infinite read loop, causing CPU exhaustion and an eventual Out-of-Memory (OOM) crash.

A Denial of Service (DoS) vulnerability in the gmrtd Go library allows a malicious NFC chip to crash reading applications by advertising a fake, massive file size (4GB+), leading to memory exhaustion.

The Hook: When the Passport Attacks You

We usually think of NFC security in one direction: a malicious reader skimming your credit card or passport in a crowded subway. We worry about shielding sleeves and cryptograms. But rarely do we consider the inverse scenario: what if the passport itself is the weapon?

Enter gmrtd, a Go library designed to handle the heavy lifting of parsing Machine Readable Travel Documents (MRTDs) via the ISO7816 protocol. It implements the complex dance of APDU commands required to authenticate and read data groups (DGs) from the chip—like your face photo or biometric fingerprints.

In a perfect world, hardware adheres to spec. In the real world, hardware lies. CVE-2026-24738 is a classic case of "implicit trust." The library assumes that if an NFC chip says, "I have a file here, and it is 4 gigabytes long," the chip is telling the truth. This assumption turns every border control kiosk or ID-verification mobile app using this library into a sitting duck for a simple resource exhaustion attack.

The Flaw: Trusting the Length Byte

The vulnerability lies in how gmrtd handles BER-TLV (Basic Encoding Rules - Tag-Length-Value) encoded data. When you read a file from an ePassport, the first thing you get is a header. This header tells you what the data is (Tag) and how big it is (Length).

In the smart card world, lengths can be encoded in "short form" (one byte) or "long form." Long form allows you to specify massive sizes by using a prefix like 0x84, followed by four bytes representing the size. This theoretically allows a file size up to 4GB (0xFFFFFFFF).

The flaw is embarrassingly simple: gmrtd parsed this length and immediately committed to reading that many bytes. It didn't ask, "Does a 4GB photo make sense on a low-power smart card?" It didn't ask, "Do I even have enough RAM to hold this?" It just shrugged and entered a loop.

This is a logic bug class known as Uncontrolled Resource Consumption (CWE-400). By failing to validate the upper bound of the length field against a sanity limit (like the max size of an ISO7816 file or available memory), the code voluntarily enters a death spiral.

The Code: The Smoking Gun

Let's look at the crime scene in iso7816/nfc_session.go. The ReadFile function is responsible for fetching the Data Group. Here is the logic before the fix:

func (nfc *NfcSession) ReadFile(fileId uint16) (fileData []byte, err error) {
    // [1] Read the header from the chip
    fileHeader, err := nfc.ReadBinary(0, 4) 
    
    // [2] Parse the Tag and Length. 
    // tmpTlvLength comes directly from the malicious chip.
    tmpTag, tmpTlvLength, tmpBuf, err := tlv.ParseTagAndLength(fileHeader)
    
    // [3] Set the target. No validation occurs here.
    totalBytes = int(tmpTlvLength)
    totalBytes += 4 - tmpBuf.Len()
 
    // [4] The Loop of Doom
    if fileBuf.Len() < totalBytes {
        for {
            // Calculate next chunk size
            bytesToRead := min(maxReadAmount, totalBytes-fileBuf.Len())
            
            // Read the chunk
            tmpData, err := nfc.ReadBinaryFromOffset(fileBuf.Len(), bytesToRead)
            
            // Append forever until OOM or totalBytes is reached
            fileBuf.Write(tmpData)
        }
    }
}

The code at [3] is the fatal error. It takes tmpTlvLength (which could be 4,294,967,295) and sets it as the goalpost. The for loop at [4] will then execute millions of times, issuing READ BINARY commands and appending the results to a byte slice until the Go runtime panics with an Out of Memory error or the CPU melts.

The Exploit: The Infinite Passport

To exploit this, you don't need a supercomputer. You need a $150 Flipper Zero or a rooted Android phone running a card emulation app. The goal is to emulate an ePassport that looks normal during the initial handshake but becomes a monster when the reader asks for data.

Here is the attack chain:

  1. Handshake: The attacker presents the emulated device. The reader (victim) performs the standard ISO7816 selection and authentication.
  2. The Trap: The reader requests DG2 (the facial image). This is standard procedure for almost all ID verification flows.
  3. The Trigger: The emulated chip responds to the first READ BINARY command with a valid Tag (0x61) but a weaponized Length: 0x84 0xFF 0xFF 0xFF 0xFF (4GB).
  4. The Stall: The reader sees the length and starts requesting chunks of 256 bytes. The attacker's emulator simply replies with 256 bytes of garbage (zeros) and a success status word (0x90 0x00) for every request.

The victim application hangs immediately. If the application is single-threaded or the reading happens on the main UI thread, the interface freezes. Eventually, the operating system kills the process for consuming too much memory.

The Fix: Trust Nobody

The remediation, applied in v0.17.2, is a lesson in defensive programming. The maintainers implemented hard limits on what the library is willing to accept from the external world. They realized that a passport photo is never going to be 4GB. In fact, it's rarely going to be larger than 30-40KB.

The patch introduces two key constraints in iso7816/nfc_session.go:

  1. Max TLV Length Cap: A constant READ_FILE_MAX_TLV_LENGTH set to 65,535 bytes (64KB). If the chip declares a size larger than this, the library immediately aborts with an error.
  2. Max Chunk Count: A constant READ_FILE_MAX_CHUNKS set to 1000. This prevents a "slow loris" style attack where the chip returns valid total lengths but sends data 1 byte at a time to waste CPU cycles.

Here is the diff for the fix:

// The Fix: Sanity Checks
if tmpTlvLength > nfc.readFileMaxTlvLength {
    return nil, fmt.Errorf("[ReadFile] TLV length exceeds permitted maximum")
}
 
// Inside the loop
if chunkCnt >= nfc.readFileMaxChunks {
    return nil, fmt.Errorf("[ReadFile] Max chunks reached")
}

This turns an OOM crash into a graceful error message: "[ReadFile] TLV length exceeds permitted maximum". The application survives, logs the error, and the attacker is left standing there looking silly.

Fix Analysis (1)

Technical Appendix

CVSS Score
4.6/ 10
CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Border Control Kiosks using Go-based verificationMobile Identity Verification Apps (Android/iOS backends)Electronic ID (eID) readersAny Go software using `gmrtd` to interact with smart cards

Affected Versions Detail

Product
Affected Versions
Fixed Version
gmrtd
gmrtd
< 0.17.20.17.2
AttributeDetail
CWE IDCWE-400 (Uncontrolled Resource Consumption)
Attack VectorPhysical (NFC)
CVSS4.6 (Medium)
ImpactDenial of Service (DoS)
Patchv0.17.2
Exploit StatusPoC Available
CWE-400
Uncontrolled Resource Consumption

The software does not properly restrict the size or amount of resources that are requested or consumed, which can be used to consume all available resources.

Vulnerability Timeline

Vulnerability discovered and patched
2026-01-26
Release v0.17.2 published
2026-01-26
CVE-2026-24738 assigned
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.