Back to Blog
critical SEVERITY4 min read

getCheckboxString() HTML Injection via MIDI Metadata

The `getCheckboxString` helper built checkbox markup by interpolating MIDI instrument and program names directly into an HTML template string, which was then parsed with `DOMParser` and injected via `replaceChildren()`. A crafted MIDI file could smuggle an HTML/JS payload through its instrument metadata and have it rendered as live DOM, including inline event handlers. The fix adds a dedicated `escapeHtml()` function and routes both parameters through it before the markup is built.

O
By Orbis AppSec
•Published October 1, 2026•Reviewed October 1, 2026

Answer Summary

The affected code is the first-party `getCheckboxString()` helper that renders MIDI instrument and program checkboxes; there is no package or version range because this is application code, not a dependency. An attacker who crafts a MIDI file with a malicious instrument or program name in its metadata could get that string injected as raw HTML into the editor's DOM when the file is loaded, enabling HTML/JavaScript injection in the victim's browser session. The fix adds an `escapeHtml()` function that HTML-entity-encodes the `name` and `label` parameters before they are interpolated into the checkbox template; there is no version number tied to this first-party change. CWE: unknown (not formally assigned in this report).

Vulnerability at a Glance

cweunknown
fixAdded `escapeHtml()` and applied it to both parameters inside `getCheckboxString()`
riskMalicious MIDI metadata can inject executable HTML into the editor UI
languageJavaScript
root cause`name` and `label` parameters interpolated into a template literal without escaping before DOM insertion
vulnerabilityHTML Injection / DOM-based Cross-Site Scripting

Introduction

A web-based MIDI/ABC editor builds a small checkbox UI for every instrument or program found in a loaded file. The function responsible, getCheckboxString(name, label), took those two values and dropped them straight into an HTML template literal — no escaping, no validation. The resulting string was then handed to DOMParser and inserted into the live page with replaceChildren().

That combination matters because name and label aren't operator-controlled configuration values — they come from parsing the instrument and program fields of an untrusted MIDI file a user opens in the editor. Any code path that turns "data extracted from a file format" into "raw HTML inserted into the DOM" without an escaping step is a textbook HTML/script injection primitive, even if no public exploit chain has been demonstrated yet.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem npm
CVE / GHSA not assigned
CWE unknown

This is application code rather than a published dependency, so there's no version range to check — if you're running a build that still contains the unescaped getCheckboxString() implementation, you're exposed.

The Vulnerability Explained

Before the fix, the function looked like this:

function getCheckboxString(name, label) {
  return `
<div class="form-check form-check-inline">
  <label class="form-check-label">
    <input class="form-check-input" name="${name}" value="${label}" type="checkbox" checked>
    ${label}
  </label>
</div>`;
}

Both name and label are interpolated twice into the markup: once inside an HTML attribute (name="${name}", value="${label}") and once as element content (${label}). Neither position has any encoding applied, and the caller feeds this template with values taken directly from a parsed MIDI file's instrument and program metadata.

The resulting string is then parsed with DOMParser and attached to the page via replaceChildren(). Developers sometimes assume that parsing through DOMParser before insertion is "safer" than innerHTML — it isn't, when the string itself already contains executable markup like an <img onerror=...> tag or a closing "> that breaks out of the attribute context.

Attack scenario: An attacker crafts a MIDI file where an instrument or program name field contains something like "><img src=x onerror=alert(document.cookie)>. When a victim opens that file in the editor, getCheckboxString() builds the checkbox markup with that payload embedded as literal HTML, DOMParser turns it into real DOM nodes, and replaceChildren() mounts it in the page — firing the onerror handler in the victim's browser session. For any site that lets users share or preview MIDI files, this turns a file-upload feature into a script-execution vector.

The Fix

The patch introduces a dedicated escaping helper and routes both parameters through it before the template string is built:

function escapeHtml(value) {
  return String(value).replace(/[&<>"']/g, (c) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
  })[c]);
}

And getCheckboxString() now uses it on both inputs:

function getCheckboxString(name, label) {
  const safeName = escapeHtml(name);
  const safeLabel = escapeHtml(label);
  return `...
    <input class="form-check-input" name="${safeName}" value="${safeLabel}" type="checkbox" checked>
    ${safeLabel}
  ...`;
}

escapeHtml() converts the five characters that matter for breaking out of an HTML attribute or element context — &, <, >, ", ' — into their entity equivalents. That means a crafted instrument name like "><img src=x onerror=...> is rendered as inert text (&quot;&gt;&lt;img src=x onerror=...&gt;) instead of being parsed as a new element. Both call sites that previously used the raw name and label were updated to use safeName and safeLabel, so the fix covers the attribute context and the element-content context in one pass, rather than patching just one of the two injection points.

Key Takeaways

  • Treat MIDI instrument and program name fields as untrusted input — they're attacker-controlled strings pulled from an uploaded file, not internal configuration.
  • Routing a string through DOMParser before inserting it with replaceChildren() does not sanitize it; a crafted onerror attribute still fires once the parsed nodes are attached.
  • Centralizing escaping in one escapeHtml() function and applying it at the single construction point (getCheckboxString()) is easier to audit than scattering ad hoc encoding across callers.
  • When a template literal interpolates the same value into both an attribute and element content, each context needs the escaped value — fixing only one leaves the other exploitable.

How Orbis AppSec Detected This

  • Source: instrument and program name fields extracted while parsing an uploaded MIDI file.
  • Sink: the HTML template literal inside getCheckboxString(), parsed with DOMParser and attached via Element.replaceChildren().
  • Missing control: no output encoding of the name and label parameters before they were embedded in HTML attribute and element-content positions.
  • CWE: unknown (not formally assigned in this finding).
  • Fix: added an escapeHtml() helper and applied it to both name and label before building the checkbox markup.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This finding is a reminder that file-format metadata is just as untrusted as a URL parameter or a form field — a MIDI instrument name is still attacker-controlled text once it reaches your rendering code. getCheckboxString() treated that text as safe markup for two separate HTML contexts, and DOMParser plus replaceChildren() didn't provide the sanitization developers might assume. Adding a single escapeHtml() helper and applying it consistently closes both injection points with a minimal, auditable change.

Prevention and further reading

Frequently Asked Questions

Where do the `name` and `label` values passed into `getCheckboxString()` originate from?

They are derived from instrument and program fields parsed out of a loaded MIDI file, not from a trusted internal source.

Does calling `DOMParser().parseFromString()` on the generated markup neutralize script execution?

No. If the string already contains an event-handler attribute like `onerror=`, parsing it into a DOM tree and inserting it with `replaceChildren()` still wires up and can trigger that handler.

Is escaping only `label` enough, or did the fix also need to cover `name`?

Both needed escaping — the diff passes `name` through `escapeHtml()` as `safeName` and `label` through it as `safeLabel`, since `name` is also interpolated into an HTML attribute.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3

Related Articles

critical

navbar.html Injection via innerHTML in loadNavbar.js

An Electron app's `loadNavbar.js` fetched `navbar.html` and wrote the response straight into `innerHTML`, so any script tags or event-handler attributes in that file would execute in the renderer. The fix adds a `sanitizeNavbarHtml` routine that parses the markup with `DOMParser` and strips `<script>` tags, `on*` attributes, and `srcdoc` before the content is injected into the DOM.

critical

addSourceInput() javascript: URL Injection in Source List Handler

The `addSourceInput()` function in the application's source list handler was assigning user-provided URLs directly to input elements without protocol validation, allowing `javascript:` URLs to persist and execute. A new `isSafeSourceUrl()` helper now restricts values to HTTP/HTTPS protocols or empty strings.

high

Location Search XSS in index.html: Unsanitized Query Rendering

A location search feature in index.html rendered user-supplied search queries directly into the DOM without HTML entity encoding, allowing attackers to inject malicious JavaScript. The fix adds output encoding that converts dangerous characters (`&`, `<`, `>`, `"`, `'`) to their HTML entity equivalents before the query string reaches the page.

high

[ValidateInput(false)] on SettingsController.Index Enables Stored XSS

The `Index` POST action of the Power BI module's `SettingsController` was decorated with `[ValidateInput(false)]`, switching off ASP.NET MVC's built-in request validation for every form field bound to `SettingsModel`. Raw `<script>` markup could therefore be persisted into module settings and later rendered back to other users. The fix removes the attribute, restoring framework-level rejection of markup-bearing input on that action while leaving its `[ValidateAntiForgeryToken]` and edit-level au

high

innerHTML Injection in postAlert(): Glitch.me Data Renders Unsanitized

The `postAlert()` function fetched alert data from a Glitch.me endpoint and injected it directly into the DOM using `innerHTML`, enabling arbitrary JavaScript execution if that external source was compromised. The fix replaces the HTML string concatenation with safe DOM API methods: `document.createTextNode()` for content and `addEventListener()` for event handlers, eliminating the injection vector entirely.

critical

i18next-fs-backend 2.6.4 Prototype Pollution via Crafted Missing-Key

A critical prototype pollution vulnerability in i18next-fs-backend 2.6.4 allows attackers to modify Object.prototype through maliciously crafted translation key strings. The fix upgrades the package from 2.6.4 to 2.6.6, eliminating the unsafe key handling that permitted this attack vector.