Blog · MCP Server Security
MCP server Web NFC security — proximity-based injection, NDEF URL payloads, and persistent permission in PWA contexts
Web NFC allows web applications to read and write NFC tags via the device's NFC hardware. In MCP Progressive Web Apps, the NDEFReader API opens a physical-world attack vector: an attacker who places a malicious NFC tag near the device can inject data into the MCP tool input stream without touching the screen or network. NDEF URL records trigger navigation to attacker-controlled origins. The nfc permission granted to an installed PWA persists across sessions without re-prompting.
NDEFReader as a proximity-based attack vector
The Web NFC API's NDEFReader interface allows an installed web application to receive data from any NFC tag the device approaches — no user interaction beyond the initial permission grant. An MCP PWA that uses NFC to receive task parameters, configuration updates, or authentication tokens can receive that same data from a malicious tag placed by an attacker in a conference room, coffee shop, or office environment.
// DANGEROUS: MCP PWA accepts tool configuration from NFC without validation
const reader = new NDEFReader();
await reader.scan();
reader.addEventListener('reading', ({ message, serialNumber }) => {
for (const record of message.records) {
if (record.recordType === 'text') {
const decoder = new TextDecoder(record.encoding || 'utf-8');
const text = decoder.decode(record.data);
// Parse as tool configuration JSON — no validation
const config = JSON.parse(text);
updateMcpServerConfig(config); // attacker-controlled object
}
if (record.recordType === 'url') {
const url = record.toURL();
// Navigate to URL from NFC tag — open redirect from physical world
window.location.href = url; // attacker controls destination
}
}
});
// Attacker plants an NFC tag in the environment containing:
// { "mcpServerUrl": "https://attacker.example/mcp", "apiKey": "" }
// or a URL record: "https://attacker.example/phishing-login"
// Device reads the tag — tool config or navigation hijacked.
Physical proximity, zero interaction: Standard NFC read range is 0–4 cm. An attacker needs to bring a tag this close — but not touch the device. In practice, a tag taped under a desk, embedded in a business card, or placed on an NFC-enabled surface (charging pad, conference room table, keyboard) achieves proximity without the user noticing. The MCP PWA reads it the moment the device passes within range.
NDEF record types and their injection payloads
NDEF (NFC Data Exchange Format) defines record types, each with different injection implications:
// NDEF record types relevant to MCP injection attacks:
// 1. TEXT record — arbitrary string injection
// Payload: JSON, commands, configuration blobs
if (record.recordType === 'text') {
const text = new TextDecoder(record.encoding).decode(record.data);
// text = '{"serverUrl":"https://attacker.example/","role":"admin"}' from malicious tag
}
// 2. URL record — navigation or protocol handler invocation
// Payload: https:// links trigger navigation; custom scheme:// triggers protocol handlers
if (record.recordType === 'url') {
const url = record.toURL();
// url = "https://attacker-phishing.example/login?redir=skillaudit.dev"
// url = "intent://mcp?server=attacker.example#Intent;scheme=mcp;end" // Android intent
}
// 3. MIME record — arbitrary typed data
// Payload: application/json, image/*, or application-specific types
if (record.recordType === 'mime') {
const mimeType = record.mediaType; // "application/vnd.mcp-config+json"
const data = record.data; // ArrayBuffer with attacker-controlled content
}
// 4. External type record — organization-namespaced custom records
// Payload: application-specific binary or text, no schema enforcement
if (record.recordType === 'skillaudit.dev:tool-config') {
// Application expected this record type — but ANY NFC tag can produce it
// There is no authentication on who wrote the tag
}
No authentication on NFC tag origin: The Web NFC API provides the physical serialNumber of the NFC tag, but this identifier is trivially cloneable — NFC tag duplicators cost under $5. An attacker who knows the serial number of a trusted tag can clone it to a new tag carrying a malicious payload. Serial number allowlisting provides no security guarantee.
NFC write operations: tag cloning and corruption
NDEFWriter (now unified into NDEFReader via reader.write()) allows the PWA to write to NFC tags. An MCP tool that asks the app to write a new configuration to an NFC tag for easy distribution enables a second attack vector: the app writes attacker-controlled content to a physical tag, effectively planting a persistent attack artifact in the physical environment.
// DANGEROUS: MCP tool controls what gets written to NFC tag
const writer = new NDEFReader();
await writer.write(toolResult.nfcPayload); // attacker-controlled NDEF message
// Effect: device writes attacker's NDEF records to the next NFC tag it touches.
// If the user's device routinely "reprograms" NFC tags (e.g., for asset tracking),
// the attacker's payload is now physically distributed on those tags — persisting
// after the MCP session ends.
// ALSO: overwriting a tag with empty/locked NDEF wipes its original content
await writer.write({ records: [{ recordType: 'empty' }] });
// Legitimate RFID card or tag is corrupted — denial-of-service in physical world
// DEFENSE: never pass NFC write payloads from tool output
// All NFC write operations must use application-defined constants:
const APPROVED_TAG_PAYLOAD = {
records: [{
recordType: 'url',
data: 'https://skillaudit.dev/verify' // hardcoded, not from tool
}]
};
// Require explicit user confirmation (additional user gesture) before NFC write
const confirmed = await showConfirmDialog('Write to NFC tag?');
if (confirmed) await reader.write(APPROVED_TAG_PAYLOAD);
Persistent nfc permission in installed PWA contexts
When a user installs an MCP PWA to their home screen (via the Web App Manifest), the browser may persist certain permissions granted during initial use. The nfc permission granted once in a browser tab may automatically carry over to the installed PWA context — allowing the PWA to call reader.scan() without re-prompting on subsequent launches. An MCP tool delivered after installation can start reading NFC without the user having ever interacted with a permission prompt in the PWA context.
// Check NFC permission state before relying on silent re-use:
const permState = await navigator.permissions.query({ name: 'nfc' });
// permState.state: 'granted', 'denied', or 'prompt'
// If 'granted' with no recent user interaction, the permission was carried over
// from a prior grant — in an installed PWA, this happens silently.
// DEFENSE: treat permission.state === 'granted' with no recent user gesture
// as a signal to re-request explicitly:
if (permState.state === 'granted') {
// Verify the user actually intended NFC access in this session
// by gating scan() on an explicit user action:
scanButton.addEventListener('click', async () => {
const reader = new NDEFReader();
await reader.scan(); // user gesture required for each session's scan
reader.addEventListener('reading', handleReading);
});
} else {
// Permission prompt required — at least the user sees it
scanButton.style.display = 'block';
}
// For revocation: use Permissions-Policy to block NFC in iframes
// <iframe src="tool-output.html" allow="nfc 'none'"></iframe>
// This prevents tool output rendered in iframes from accessing NFC
Web NFC security — risk comparison
| Pattern | Attack | Physical requirement | Defense |
|---|---|---|---|
Continuous NDEFReader.scan() without user gesture per read |
Any NFC tag within 4 cm range injects data; attacker plants tag in environment | Tag within 4 cm of device at any point during app session | Gate scan() on explicit user gesture per session; parse records with strict schema validation |
| URL record triggers navigation or protocol handler | Physical tag navigates MCP PWA to attacker-controlled origin; phishing or protocol handler abuse | Same as above | Never use record.toURL() for navigation; treat URL records as untrusted strings, apply URL allowlist validation |
| NFC write payload from MCP tool output | App writes attacker content to physical NFC tags in the environment; persistent physical attack artifacts | User device touches a writable NFC tag | Hardcode all NFC write payloads; require explicit user confirmation before write |
Persistent nfc permission in installed PWA |
Permission silently carried from prior grant; MCP tool starts NFC scan without user prompt | One-time permission grant; physical proximity for reading | Check permission state on each session; re-gate scan on user gesture even when permission is 'granted' |
| Serial number allowlist as NFC authentication | Attacker clones allowed tag serial number to new tag carrying malicious payload | Requires observing serial number of one allowed tag (trivial) | Do not use serial number as a trust signal; sign NDEF record payloads with HMAC or ECDSA; verify signature before processing |
SkillAudit findings for Web NFC usage
Audit your MCP server Web NFC security
SkillAudit detects NDEFReader usage without payload validation, URL record navigation without allowlisting, NFC write operations from tool output, persistent permission without re-gating, and serial number authentication patterns. Free audit in 60 seconds.
Free audit →