Blog · 2026-06-20 · WebRTC · DTLS · MCP Servers
MCP Server WebRTC Data Channel Security: DTLS Fingerprint Verification, ICE Candidate Injection, SDP Tampering, and Signaling Server Authentication
WebRTC data channels skip the server-side relay — that's the entire value proposition for high-frequency MCP tool delivery between peers. But removing the relay also removes the central point where you authenticate both sides, inspect traffic, and terminate TLS. The security work moves to the DTLS handshake, the SDP exchange, and the signaling server. Each of these has a distinct failure mode that silently breaks the security model while leaving the connection alive and transferring data.
Why WebRTC for MCP tool delivery
Standard MCP deployments route every tool call through a server: the client sends a JSON-RPC request over HTTP or stdio, the server executes the tool, returns the result. For latency-sensitive workflows — agent-to-agent tool calls, streaming LLM output piped into downstream tool invocations, real-time collaborative sessions — the server round-trip adds 50–200 ms per hop that compounds across a pipeline.
WebRTC data channels solve this by establishing a peer-to-peer connection with DTLS-SRTP encryption and bypassing the relay server after the initial handshake. A pair of MCP agents can exchange tool calls directly with sub-10 ms latency once the connection is established. The pattern appears in agentic systems where a "coordinator" agent dispatches tool calls to "worker" agents, and in multi-modal pipelines where audio/video streams and MCP tool calls share the same peer connection.
The threat model for this deployment pattern is categorically different from HTTP-based MCP. There is no server-side TLS termination point where you can inspect headers, enforce rate limits, or authenticate each request. Security depends on four things: the integrity of the SDP exchange, the verification of the DTLS certificate fingerprint, the authentication of the signaling channel, and the validation of messages on the data channel. If any of these fails silently, the connection continues to work — it just works in the attacker's favor.
The DTLS fingerprint attack: encryption without authentication
DTLS-SRTP is the transport security layer for WebRTC. Every peer generates a self-signed certificate, computes its SHA-256 fingerprint, and includes that fingerprint in the SDP offer or answer it sends via the signaling server. The receiving peer stores the expected fingerprint, waits for the DTLS handshake to complete, and then verifies that the certificate presented during the handshake matches the fingerprint from the SDP.
The vulnerability is in the last step: that verification is not always performed. Browser WebRTC implementations do it automatically. Custom WebRTC implementations using libraries like node-webrtc, pion/webrtc (Go), or aiortc (Python) require you to perform it yourself — or configure the library to enforce it. If you skip verification, the DTLS handshake succeeds with any certificate, including one presented by a man-in-the-middle who received the SDP (and the legitimate peer's fingerprint) via the signaling server.
Root cause: DTLS provides transport encryption. It only provides peer authentication when you verify that the certificate in the handshake matches the fingerprint from the SDP. Without verification, DTLS is encrypted but unauthenticated — a MITM can decrypt and re-encrypt all traffic with their own certificate.
The attack proceeds as follows:
SDP interception: Attacker compromises the signaling server (or runs a malicious signaling proxy). They receive Alice's SDP offer including her DTLS fingerprint (a=fingerprint:sha-256 AA:BB:CC:...).
Fingerprint replacement: Attacker generates their own DTLS certificate. They replace Alice's fingerprint in the SDP with their own (a=fingerprint:sha-256 EE:FF:00:...) and forward the modified SDP to Bob.
Dual DTLS handshakes: Attacker completes a DTLS handshake with Alice (presenting Alice's certificate — stolen from the original SDP — is not required; presenting any certificate works if verification is skipped). They also complete a separate DTLS handshake with Bob presenting their own certificate.
Transparent relay: Every MCP tool call from Alice arrives at the attacker decrypted, is logged or modified, and is re-encrypted and forwarded to Bob. Both sides believe they have a secure peer connection. Tool call results flow back through the same relay.
The defense is mandatory fingerprint verification after the DTLS handshake. In pion/webrtc:
// After creating the PeerConnection and before sending/receiving data
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
if s == webrtc.PeerConnectionStateConnected {
// Get the remote certificate fingerprint from the DTLS transport
dtlsTransport := pc.SCTP().Transport()
remoteFingerprint, err := dtlsTransport.GetRemoteCertificate()
if err != nil {
log.Fatal("DTLS connected but no remote certificate:", err)
}
// Compare against the fingerprint received in the SDP (stored at offer time)
expectedFingerprint := session.RemoteSDP.GetFingerprint() // your parser
actualFingerprint := sha256Hex(remoteFingerprint)
if actualFingerprint != expectedFingerprint {
log.Fatal("DTLS fingerprint mismatch — possible MITM, closing connection")
pc.Close()
}
}
})
In aiortc (Python), set RTCConfiguration(certificates=[...]) on both sides and use a custom fingerprint verifier in the on_track / datachannel callbacks. The point is not the library — it's that you must extract the presented certificate after the handshake and compare its fingerprint to what you received in the SDP, in code you control.
ICE candidate injection: forcing the connection path
ICE (Interactive Connectivity Establishment) candidates describe the IP address + port combinations where each peer can be reached. A typical ICE gathering phase produces three candidate types: host candidates (LAN IP), server-reflexive candidates (public IP via STUN), and relay candidates (via a TURN server).
The signaling server relays ICE candidates between peers alongside the SDP. If the signaling server is unauthenticated or if ICE candidates can be injected via a compromised session, an attacker can inject relay candidates pointing to an attacker-controlled TURN server. The ICE negotiation follows the highest-priority candidates that result in a working connection. If the attacker's relay candidate is accepted, all media and data channel traffic flows through the attacker's relay — even if DTLS is properly verified (the attacker relays encrypted data without decrypting it, but can perform traffic analysis, throttle, or disrupt).
Combined attack: ICE candidate injection combined with a DTLS certificate substitution (if fingerprint verification is skipped) gives the attacker plaintext access to all data channel traffic. Even with fingerprint verification, ICE injection achieves traffic analysis and service disruption.
The defense is twofold. First, authenticate and integrity-protect all signaling messages including ICE candidates (covered below). Second, configure ICE transport policy to restrict the candidate types your application accepts:
// Restrict to relay candidates only (for environments where direct P2P is unsafe)
const pc = new RTCPeerConnection({
iceTransportPolicy: 'relay', // only TURN relay candidates accepted
iceServers: [
{
urls: 'turn:your-turn-server.internal:3478',
username: session.turnCredential.username, // short-lived
credential: session.turnCredential.password // short-lived HMAC
}
]
});
// Or for controlled P2P environments, restrict to expected IP ranges
// via a custom ICE candidate filter
pc.addEventListener('icecandidate', (event) => {
if (!event.candidate) return;
const candidate = event.candidate;
if (!isAllowedCandidateAddress(candidate.address)) {
// Don't send this candidate to the remote peer
return;
}
signalingServer.send({ type: 'ice-candidate', candidate });
});
When iceTransportPolicy: 'relay' is set, the browser/library will only use relay candidates, which means all traffic goes through your TURN server — a server you control and authenticate. This trades direct P2P performance for routing assurance. In an MCP deployment where the tool calls carry sensitive data, that trade is usually correct.
SDP offer/answer tampering: downgrading the connection
The SDP (Session Description Protocol) blob contains everything about a WebRTC session: codec preferences, ICE credentials, DTLS fingerprints, and media/data channel configuration. If an attacker can modify the SDP between peers, they can:
- Replace the DTLS fingerprint (enabling the MITM attack described above)
- Strip the
a=setup:actpassattribute, forcing a specific DTLS role that benefits the attacker's handshake position - Remove host and server-reflexive ICE candidates, forcing the connection through a relay (where they have visibility)
- Add ICE candidates pointing to attacker infrastructure
- Modify ICE credentials (
a=ice-ufrag,a=ice-pwd) to impersonate the ICE negotiation
SDP tampering is only possible if the signaling channel lacks integrity protection. The defense is to sign SDP blobs and ICE candidates before sending them through the signaling server and verify signatures on receipt. For MCP deployments, this means:
// Server-side: sign the SDP before relaying
function signSDP(sdpBlob, sessionKey) {
const ts = Date.now();
const payload = JSON.stringify({ sdp: sdpBlob, ts });
const sig = crypto.createHmac('sha256', sessionKey).update(payload).digest('hex');
return { sdp: sdpBlob, ts, sig };
}
// Client-side: verify before applying remote description
async function applyRemoteDescription(signed) {
const { sdp, ts, sig } = signed;
if (Date.now() - ts > 30_000) throw new Error('SDP too old — replay?');
const expected = hmacSha256(sessionKey, JSON.stringify({ sdp, ts }));
if (!timingSafeEqual(expected, sig)) throw new Error('SDP signature invalid');
await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp }));
}
The session key must be established through a channel independent of the WebRTC signaling path — typically a pre-existing authenticated API session or a key exchange that predates the WebRTC negotiation. If the key exchange itself is unauthenticated, signing the SDP provides no protection.
TURN server credential exposure
TURN (Traversal Using Relays around NAT) servers relay WebRTC traffic when direct peer-to-peer connections are blocked by firewalls or NAT. Your signaling server must issue TURN credentials to both peers before the ICE gathering phase. If these credentials are long-lived or static, exposure in client-side JavaScript, server logs, error responses, or analytics pipelines gives anyone who finds them access to your TURN infrastructure as a relay — bandwidth theft and TURN server DoS.
TURN credentials must be short-lived and session-specific. The standard approach uses HMAC-based time-limited credentials:
// Server-side TURN credential generation (per RFC 8489 long-term credential variant)
function generateTurnCredential(userId, ttlSeconds = 3600) {
const username = `${Math.floor(Date.now() / 1000) + ttlSeconds}:${userId}`;
const password = crypto
.createHmac('sha1', process.env.TURN_SECRET)
.update(username)
.digest('base64');
return { username, password, ttl: ttlSeconds };
}
// Client receives these from your authenticated API, not hardcoded in JS
const { username, password } = await fetchFromAuthenticatedAPI('/api/turn-credentials');
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'turn:turn.yourdomain.com:3478', username, credential: password }]
});
The TURN server verifies the timestamp embedded in the username and the HMAC signature derived from your TURN_SECRET. Credentials expire after ttl seconds. An attacker who intercepts credentials can only relay traffic for the remaining lifetime of the credential — not indefinitely.
Additional controls: limit TURN bandwidth per credential, restrict TURN peer addresses to your own IP ranges (so TURN can't be used to relay traffic to arbitrary internet hosts), and rotate TURN_SECRET monthly.
Signaling server authentication: the root of trust
The signaling server is the root of trust for the entire WebRTC session. It is the channel through which SDP offers, SDP answers, and ICE candidates flow. If the signaling server does not authenticate both peers before relaying messages between them, any party who can reach the signaling server's WebSocket endpoint can inject themselves into any session.
The attack: attacker connects to the signaling server, claims to be Bob (Alice's intended peer), and either shares Alice's session room ID (which they obtained through a phishing link, a leaked share URL, or enumeration of sequential room IDs) or creates a new room and tricks Alice into joining it. Alice's SDP offer goes to the attacker. The attacker replaces the DTLS fingerprint, forwards it to the legitimate Bob, and completes the MITM relay.
Signaling server authentication requirements for MCP deployments:
Authenticate the signaling WebSocket
Require a valid JWT or session token in the WebSocket upgrade request (Authorization header or ?token= query param). Reject unauthenticated upgrades with 401 before the WebSocket is established. The token must identify the peer and be issued by your authentication system.
Cryptographically bind sessions
Session room IDs must be unguessable (128-bit random, not sequential integers). Each room is bound to a specific pair of peer identities. The signaling server rejects a WebSocket connection from peer C attempting to join a room created for A and B — even if C has a valid authentication token.
Sign all signaling messages
Each SDP and ICE candidate message must include an HMAC or signature binding it to the authenticated sender's identity. The receiving peer verifies the signature before applying the SDP or ICE candidate. This is the defense against a compromised signaling server relaying modified messages.
Session time limits
Signaling sessions must expire. A WebRTC session for an MCP tool call exchange should not stay signaling-alive indefinitely. Set a maximum session duration (e.g., 30 minutes) after which the signaling server closes the room and the DTLS connection must be re-established with fresh credentials.
A minimal authenticated signaling server pattern:
// Express + ws (Node.js signaling server)
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', async (req, socket, head) => {
// Authenticate before WebSocket upgrade
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
if (!token) { socket.destroy(); return; }
let peerId;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
peerId = payload.sub;
} catch {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
ws.peerId = peerId;
wss.emit('connection', ws, req);
});
});
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
// Verify the session room is bound to this peer's identity
const room = rooms.get(msg.roomId);
if (!room || !room.peers.includes(ws.peerId)) {
ws.send(JSON.stringify({ error: 'unauthorized_room' }));
return;
}
// Relay only to the other authenticated peer in the room
const target = room.peers.find(p => p !== ws.peerId);
target?.ws.send(JSON.stringify({ ...msg, from: ws.peerId }));
});
});
Data channel message validation
DTLS-SRTP proves that the data came from the peer who completed the handshake with the verified certificate. It does not validate the content of messages on the data channel. A legitimate but compromised peer (e.g., a worker agent running a malicious MCP skill) can send tool call results containing prompt injection payloads, oversized blobs, or malformed JSON-RPC that crashes the receiving parser.
All data channel messages must be validated before processing, exactly as you would validate HTTP request bodies:
dataChannel.addEventListener('message', (event) => {
let msg;
try {
msg = JSON.parse(event.data);
} catch {
// Malformed JSON — log and discard, do not crash
console.error('Invalid JSON on data channel from peer', peerId);
return;
}
// Enforce size limit before schema validation
if (event.data.length > MAX_MESSAGE_BYTES) {
console.error('Oversized data channel message from', peerId, event.data.length);
return;
}
// Validate with Zod schema
const result = McpToolCallSchema.safeParse(msg);
if (!result.success) {
sendError(dataChannel, { code: -32600, message: 'Invalid Request', details: result.error });
return;
}
// Now safe to process the validated tool call
handleToolCall(result.data);
});
const MAX_MESSAGE_BYTES = 64 * 1024; // 64 KB limit per message
The size limit is particularly important for WebRTC data channels: there is no server-side request body size limit because there is no server in the data path. An unvalidated data channel is the equivalent of an HTTP server with no body size limit and no Content-Type validation.
The signaling/DTLS/data-channel threat model, summarized
| Layer | Attack | Consequence | Defense |
|---|---|---|---|
| Signaling | Unauthenticated WebSocket — attacker joins any session | Full SDP/ICE injection capability | JWT auth on WebSocket upgrade; room bound to peer pair |
| SDP exchange | DTLS fingerprint replacement in SDP | MITM with plaintext access to all tool calls | HMAC-sign SDP; verify fingerprint after DTLS handshake |
| ICE negotiation | Inject ICE candidates pointing to attacker relay | Traffic analysis, disruption; plaintext if DTLS also compromised | Sign ICE candidates; iceTransportPolicy:'relay' with your TURN |
| TURN credentials | Static or long-lived credentials extracted from client JS | Attacker uses your TURN as a relay for arbitrary traffic | Short-lived HMAC-based credentials per session; bandwidth caps |
| DTLS handshake | Fingerprint not verified — MITM certificate accepted | MITM decrypts all data channel traffic | Always verify presented certificate fingerprint matches SDP |
| Data channel messages | Malformed JSON, oversized payload, prompt injection in result | Parser crash, DoS, prompt injection propagation | Size limit + Zod schema validation on every message |
SkillAudit findings for WebRTC MCP deployments
Deployment checklist
- Signaling WebSocket requires JWT authentication on upgrade; unauthenticated connections rejected with HTTP 401 before WebSocket is established
- Session room IDs are 128-bit random values; not sequential, not derived from predictable inputs
- Each room is bound to a specific pair of authenticated peer identities; third peers rejected even with valid auth tokens
- All SDP blobs and ICE candidates are HMAC-signed by the sender before relaying; receiver verifies signature before applying
- DTLS certificate fingerprint is verified after handshake in all non-browser implementations; connection closed on mismatch
- TURN credentials are short-lived (≤ 1 hour) HMAC-based credentials generated per session by the authenticated signaling server
TURN_SECRETis not present in client-side JavaScript or any client-accessible resource- TURN server restricts peer addresses to known IP ranges; cannot be used to relay to arbitrary internet hosts
- Data channel message handler enforces a hard byte-length limit before JSON parsing
- All data channel messages validated against a strict Zod/JSON Schema before processing
- Signaling sessions expire after a configured maximum lifetime; reconnection requires fresh credentials
iceTransportPolicy: 'relay'configured for MCP deployments where P2P path is not required to be direct
Summary
WebRTC data channels are compelling for high-frequency MCP tool delivery between agents, but the security model is entirely different from HTTP-based MCP. There is no server-side TLS termination that authenticates every request. Instead, the security chain is: authenticated signaling → signed SDP with HMAC-integrity → DTLS fingerprint verification → message-level validation on the data channel. The chain fails silently at every link — a DTLS connection with an unverified fingerprint still transfers data; an unauthenticated signaling session still establishes WebRTC connections; unsigned SDP still negotiates correctly. The failures are invisible until someone abuses them.
The most important control is the first one: authenticate the signaling WebSocket. Everything else in the chain — SDP integrity, fingerprint verification, ICE candidate trust — depends on the signaling session being bound to authenticated peer identities. If signaling is unauthenticated, the attacker can replace the entire SDP, not just the fingerprint, and the entire cryptographic chain is broken regardless of how carefully you implemented DTLS verification.
See MCP Server Message Queue Security for the analogous threat model in async broker-based deployments. See MCP server fetch() API security for the HTTP-based transport security patterns that apply when you're not using WebRTC.
Run a free SkillAudit scan on your MCP server to detect missing DTLS fingerprint verification, unauthenticated signaling patterns, unsigned SDP relay, and data channel validation gaps — before you publish to the Anthropic skills directory. Audit your MCP server →