CVE-2026-17910
Overview
Background
- Web NFC
- a web platform API that lets a page read and write NFC tags via the browser, gated by a blocklist that forbids interaction with sensitive tags.
- `NfcBlocklist`
- the Android-side allow/deny logic in
services/device/nfc/androidthat decides whether a discovered tag may be exposed to a web page. - Historical bytes
- the ISO 14443-4 ATS field a smart card returns; for a
YubiKey 5its indices 2-4 are card-capability flags and the trailing bytes are a proprietary"YubiKey"info field. - YubiKey
- a hardware security key that must never be scriptable over Web NFC, hence its presence on the blocklist.
Root Cause Analysis
NfcBlocklist.areHistoricalBytesBlocked identified YubiKey 5 tags by comparing the tag’s full historical-bytes sequence against a single hardcoded STATIC_HISTORICAL_BYTES array using exact equality. That array baked in the card-capability flag bytes at indices 2-4, but those flags are not stable — they drift across firmware revisions, so a YubiKey 5 running newer or older firmware emits a different full sequence. Because the match required Arrays.equals over the entire array, any drift in the capability bytes caused the comparison to fail and the tag to fall through as not-blocked. The device was therefore exposed to Web NFC despite being a known-sensitive security key, defeating the policy entirely.
The fix replaces the exact match with a suffix comparison (endsWith) against YUBIKEY_5_HISTORICAL_BYTES_SUFFIX — the invariant proprietary 0x57 + "YubiKey" info field — so recognition no longer depends on the volatile flag bytes.
Attack Path
- Obtain a drifted key
An attacker or victim holds a
YubiKey 5whose firmware emits capability-flag bytes differing from the hardcoded sequence. - Present the tag A malicious page requests Web NFC and the user taps the security key to the device.
- Match fails open
areHistoricalBytesBlockedcompares the full sequence, the differing indices 2-4 breakArrays.equals, and the tag is treated as not blocked. - Interact with the key The page is granted NFC access to a device the blocklist was specifically designed to protect.
Impact Assessment
YubiKey 5 security keys that should be categorically blocked, expanding attack surface against hardware authenticators. Exploitation requires only a physical tap of an affected-firmware key against the device, with no additional privilege. The blast radius is bounded to YubiKey 5 units whose historical bytes diverge from the retired hardcoded sequence.Changed Functions
| Function | Change | Notes |
|---|---|---|
forservices/device/nfc/android/java/src/org/chromium/device/nfc/NfcBlocklist.java |
modified | |
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
- Exact-match device signaturesAudit every blocklist/allowlist that identifies hardware by
Arrays.equalsover a full byte sequence; flag any that include firmware- or configuration-variable fields and prefer matching on stable identity substrings. - Fail-open policy checksReview NFC and other capability gates where a failed identity comparison results in access being granted rather than denied, and confirm unknown or malformed inputs default to blocked.
- Suffix/prefix boundary handlingVerify the new
endsWithhelper correctly handles null, short, and equal-length arrays so the suffix logic cannot be evaded by length manipulation.
Patch
From 13ae2a29048162dbae9867c5d8ef74bbfd922e21 Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> Date: Wed, 10 Jun 2026 15:38:30 -0700 Subject: [PATCH] nfc: Fix Web NFC Blocklist bypass for YubiKeys Use suffix matching for YubiKey-5 historical bytes to make the blocklist robust against capability flag drift caused by firmware updates. Bug: 501749600 Change-Id: I62b6146f6244beeee5500a9b04b281479fbb1fbc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7916451 Reviewed-by: Fr <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1644931} --- 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 f13e54ff..5c43d0d 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 @@ -28,25 +28,16 @@ public class NfcBlocklist { private static final String TAG = "NfcBlocklist"; - private static final byte[][] STATIC_HISTORICAL_BYTES = { - new byte[] { - (byte) 0x80, - 0x73, - (byte) 0xc0, - 0x21, - (byte) 0xc0, - 0x57, - 0x59, - 0x75, - 0x62, - 0x69, - 0x4b, - 0x65, - 0x79 - }, // YubiKey 5 series - new byte[] { - (byte) 0x59, 0x75, 0x62, 0x69, 0x6b, 0x65, 0x79, 0x4e, 0x45, 0x4f, 0x72, 0x33 - } // YubiKey NEO + // The YubiKey 5 historical bytes end with the proprietary information field: + // 0x57 (compact-TLV Tag 5, Length 7) followed by the ASCII bytes for "YubiKey". + // We match starting from 0x57 to ignore the preceding card capability flags + // (indices 2-4 in the full sequence) which can change with firmware updates. + private static final byte[] YUBIKEY_5_HISTORICAL_BYTES_SUFFIX = { + 0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79 + }; + + private static final byte[] YUBIKEY_NEO_HISTORICAL_BYTES = { + (byte) 0x59, 0x75, 0x62, 0x69, 0x6b, 0x65, 0x79, 0x4e, 0x45, 0x4f, 0x72, 0x33 }; private static final String TRIAL_NAME = "WebNFCBlockList"; @@ -115,8 +106,8 @@ } /** - * Returns true if tag is blocked, otherwise false. A tag is blocked if it is part of - * STATIC_HISTORICAL_BYTES or server provided historical bytes. + * Returns true if tag is blocked, otherwise false. A tag is blocked if its historical bytes + * match a blocked device (e.g. YubiKeys) or server provided historical bytes. * * @see android.nfc.Tag * @return true if tag is blocked, otherwise false. @@ -144,15 +135,20 @@ } /** - * Returns true if historical bytes are part of STATIC_HISTORICAL_BYTES or server provided + * Returns true if historical bytes match a blocked device (e.g. YubiKeys) or server provided * historical bytes. * * @return true if historical bytes are blocked, otherwise false. */ @VisibleForTesting - boolean areHistoricalBytesBlocked(byte[] historicalBytes) { - for (int i = 0; i < STATIC_HISTORICAL_BYTES.length; i++) { - if (Arrays.equals(historicalBytes, STATIC_HISTORICAL_BYTES[i])) return true; + boolean areHistoricalBytesBlocked(byte @Nullable [] historicalBytes) { + if (historicalBytes == null) return false; + + if (endsWith(historicalBytes, YUBIKEY_5_HISTORICAL_BYTES_SUFFIX)) { + return true; + } + if (Arrays.equals(historicalBytes, YUBIKEY_NEO_HISTORICAL_BYTES)) { + return true; } for (int i = 0; i < mServerProvidedHistoricalBytes.size(); i++) { if (Arrays.equals(historicalBytes, mServerProvidedHistoricalBytes.get(i))) return true; @@ -160,6 +156,16 @@ return false; } + private static boolean endsWith(byte[] array, byte[] suffix) { + if (array.length < suffix.length) return false; + for (int i = 0; i < suffix.length; i++) { + if (array[array.length - suffix.length + i] != suffix[i]) { + return false; + } + } + return true; + } + /** Block/unblock NFC tag access for testing use only. */ public void setIsTagBlockedForTesting(Boolean blocked) { mIsTagBlockedForTesting = blocked; 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 aeb7bb2..20c8ac7 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 @@ -80,6 +80,48 @@ assertFalse(areHistoricalBytesBlocked(new byte[] {0x01, 0x02, 0x03})); } + /** + * Verifies that the blocklist successfully blocks YubiKey 5 devices even when their ISO 7816-4 + * card-capability flags change (drift) due to firmware updates. Instead of an exact match, we + * check for a suffix match of the proprietary 'YubiKey' identifier string (preceded by the 0x57 + * TLV tag/length), ignoring the volatile capability flags. + * + * <p>Hardcoded entry: 80 73 c0 21 c0 57 'YubiKey' + * + * <p>Variant tested: 80 73 c0 21 c1 57 'YubiKey' (capability byte 4: c0->c1) + */ + @Test + @Feature({"NfcBlocklistTest"}) + public void testYubiKey5CapabilityByteDriftBypassesBlocklist() { + NfcBlocklist.overrideNfcBlocklistForTests(/* serverProvidedValues= */ null); + + // Control: the exact 2020 fingerprint is blocked. + assertTrue(areHistoricalBytesBlocked(YUBIKEY_5_SERIES_HISTORICAL_BYTES)); + + // Same device family, one capability-flag bit flipped (c0 -> c1). + byte[] driftedYubiKey5 = + new byte[] { + (byte) 0x80, + 0x73, + (byte) 0xc0, + 0x21, + (byte) 0xc1, + 0x57, + 0x59, + 0x75, + 0x62, + 0x69, + 0x4b, + 0x65, + 0x79 // "YubiKey" + }; + // Robust blocklist matches the 'YubiKey' issuer-data suffix (including 0x57) + // to ignore capability-flag drift. + assertTrue( + "YubiKey 5 with drifted capability byte should still be blocked", + areHistoricalBytesBlocked(driftedYubiKey5)); + } + @Test @Feature({"NfcBlocklistTest"}) public void testHistoricalBytesWithInvalidProvidedServerValues() {