CVE-2026-17904
Overview
Background
- Web NFC blocklist
- A
NfcBlocklistallowlist-denial layer that refuses WebNFC access to security-sensitive tags (e.g. YubiKeys / FIDO tokens) so a malicious page cannot silently interact with them. - `IsoDep.getHistoricalBytes()`
- Android NFC API returning the ISO 14443 Type A historical bytes used to fingerprint a tag; returns
null/empty for NFC-B (Type B) tags. - `IsoDep.getHiLayerResponse()`
- Android NFC API returning the ISO 14443 Type B higher-layer response (ATTRIB) bytes, the correct identifier source for NFC-B tags.
- NFC-B (ISO 14443 Type B)
- A contactless card protocol variant that carries no historical bytes, so identity must be read from the hi-layer response instead.
Root Cause Analysis
NfcBlocklist.isTagBlocked fingerprinted every IsoDep tag solely via getHistoricalBytes() and passed the result to areHistoricalBytesBlocked(). This assumed all IsoDep tags expose historical bytes, but NFC-B (Type B) tags return null/empty there and instead carry their identifier in getHiLayerResponse(). A blocked device presenting over the NFC-B protocol therefore yielded null historical bytes, matched nothing in the blocklist, and was treated as an ordinary tag. The blocklist enforcement silently fell through to “allow,” defeating the security control for an entire class of tags.
The fix falls back to getHiLayerResponse() whenever historical bytes are null or empty, so NFC-B blocked tags are matched before access is granted.
Attack Path
- Present blocked device as NFC-B An attacker (or a lured victim tapping a token) exposes a blocklisted secure tag, e.g. a FIDO/YubiKey, over the ISO 14443 Type B protocol.
- Null historical bytes
getHistoricalBytes()returnsnull/empty for the NFC-B tag, soareHistoricalBytesBlocked()finds no match. - Blocklist fails open
isTagBlockedreturns not-blocked, so WebNFC treats the sensitive tag as permitted. - Malicious page interacts A hostile web page reads from / writes to the security-sensitive tag via WebNFC, bypassing the protection intended to isolate it.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java |
modified |
Files Changed
services/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.javaservices/device/nfc/android/junit/src/org/chromium/device/nfc/NfcBlocklistTest.java
Audit Directions
- Identifier source assumptionsAudit every security check keyed on one protocol-specific fingerprint (
getHistoricalBytes, ATR, ATQA/SAK, UID) for tag/card classes where that source is absent, and confirm a missing identifier is treated as “unidentified,” not “safe.” - Fail-open deny-listsReview blocklist/allowlist enforcement where a null or empty lookup key short-circuits to permit; ensure unmatched or unreadable inputs default to the restrictive outcome.
- NFC-B / protocol-variant coverageCheck other NFC and hardware-token code paths that branch on Type A vs Type B (
IsoDep,NfcA,NfcB) to verify both variants are fingerprinted and gated consistently.
Patch
From 9491c00b2c1910c2cafbbc25733ddbbc6507f62d Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Wed, 10 Jun 2026 11:44:07 -0700
Subject: [PATCH] nfc: Fix Web NFC Blocklist bypass for NFC-B tags
Consult getHiLayerResponse() if getHistoricalBytes() returns null or
empty, which is the case for NFC-B tags.
BUG=497337759
Change-Id: I1aa00301707679f1cf401ecd3b9568c5ab858ab9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7913540
Reviewed-by: Fr <beaufort.francois@gmail.com>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644747}
---
diff --git a/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java b/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java
index 165b21dc..f13e54ff 100644
--- a/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java
+++ b/services/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java
@@ -130,8 +130,11 @@
IsoDep iso = IsoDep.get(tag);
if (iso != null) {
- byte[] historicalBytes = iso.getHistoricalBytes();
- if (areHistoricalBytesBlocked(historicalBytes)) {
+ byte[] identifierBytes = iso.getHistoricalBytes();
+ if (identifierBytes == null || identifierBytes.length == 0) {
+ identifierBytes = iso.getHiLayerResponse();
+ }
+ if (areHistoricalBytesBlocked(identifierBytes)) {
Log.w(TAG, "Access to NFC tag is blocked.");
return true;
}
diff --git a/services/device/nfc/android/junit/src/org/chromium/device/nfc/NfcBlocklistTest.java b/services/device/nfc/android/junit/src/org/chromium/device/nfc/NfcBlocklistTest.java
index 181f614..aeb7bb2 100644
--- a/services/device/nfc/android/junit/src/org/chromium/device/nfc/NfcBlocklistTest.java
+++ b/services/device/nfc/android/junit/src/org/chromium/device/nfc/NfcBlocklistTest.java
@@ -101,4 +101,20 @@
assertFalse(areHistoricalBytesBlocked(new byte[] {}));
assertFalse(areHistoricalBytesBlocked(new byte[] {0x01, 0x02, 0x03}));
}
+
+ /**
+ * Verifies that null historical bytes are not blocked.
+ *
+ * <p>null is returned by IsoDep.getHistoricalBytes() for NFC-B tags, which are instead
+ * identified by their hi-layer response in NfcBlocklist.isTagBlocked().
+ */
+ @Test
+ @Feature({"NfcBlocklistTest"})
+ public void testNullHistoricalBytesAreNotBlocked() {
+ // Even with a server-pushed entry, null (the value every NFC-B ISO-DEP
+ // tag yields from getHistoricalBytes()) is never blocked.
+ NfcBlocklist.overrideNfcBlocklistForTests("8073c021c057597562694b6579");
+ assertFalse(areHistoricalBytesBlocked(null));
+ assertTrue(areHistoricalBytesBlocked(YUBIKEY_5_SERIES_HISTORICAL_BYTES));
+ }
}