Jan 27, 2026·6 min read·7 visits
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.
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 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.
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.
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:
DG2 (the facial image). This is standard procedure for almost all ID verification flows.READ BINARY command with a valid Tag (0x61) but a weaponized Length: 0x84 0xFF 0xFF 0xFF 0xFF (4GB).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 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:
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.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.
CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
gmrtd gmrtd | < 0.17.2 | 0.17.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) |
| Attack Vector | Physical (NFC) |
| CVSS | 4.6 (Medium) |
| Impact | Denial of Service (DoS) |
| Patch | v0.17.2 |
| Exploit Status | PoC Available |
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.