Back to Blog
critical SEVERITY5 min read

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

O
By Orbis AppSec
Published August 12, 2026Reviewed August 12, 2026

Answer Summary

This is a stored Cross-Site Scripting (XSS) vulnerability (CWE-79) in JavaScript where GitHub API commit messages are interpolated directly into `innerHTML` without sanitization in `js/main.js`. The fix applies HTML output encoding—replacing `&`, `<`, `>`, `"`, and `'` with their HTML entity equivalents—before inserting the `msg` variable into the DOM, preventing any injected HTML or script from executing.

Vulnerability at a Glance

cweCWE-79
fixHTML entity encoding applied to commit message before DOM insertion
riskArbitrary JavaScript execution in visitors' browsers via malicious commit messages
languageJavaScript
root causeUnsanitized GitHub API response data interpolated directly into innerHTML
vulnerabilityCross-Site Scripting (XSS) via innerHTML injection

Introduction

The js/main.js file in this project handles rendering a changelog by fetching commit data from the GitHub API and displaying it in a list. However, a flaw in the genChangeLogContent function at line 18 created a critical security risk: the msg variable—containing the raw commit message from GitHub's API response—was interpolated directly into an innerHTML assignment without any form of sanitization or encoding.

This meant that any HTML or JavaScript embedded in a commit message would be parsed and executed by the browser. For developers building dashboards, changelogs, or any UI that displays data from external APIs, this is a textbook example of why output encoding is non-negotiable.

The Vulnerability Explained

The Dangerous Code Pattern

Here's the vulnerable code from js/main.js inside the genChangeLogContent function:

changeLogContent.innerHTML += `
    <li class="text-white mb-2">
        ${formattedDate} | "${msg}"
    </li>
`;

The msg variable comes directly from the GitHub API's commit message field. The code uses innerHTML with a template literal, which means any HTML tags or event handlers embedded in the commit message are treated as live markup by the browser.

The Attack Scenario

Consider this concrete exploitation path:

  1. An attacker with write access to the repository pushes a commit with this message:
    <img src=x onerror=alert(document.cookie)>

  2. When any user visits the page that renders the changelog, the genChangeLogContent function fetches commits from the GitHub API.

  3. The malicious commit message is interpolated directly into innerHTML.

  4. The browser parses the <img> tag, fails to load the src=x image, and fires the onerror handler—executing arbitrary JavaScript.

This is a stored XSS attack because the payload persists in the repository's commit history and executes for every visitor. The attacker doesn't need to trick users into clicking a link; simply visiting the page triggers the exploit.

Real-World Impact

  • Session hijacking: document.cookie exfiltration sends authentication tokens to an attacker-controlled server
  • Credential theft: Injected scripts can overlay fake login forms
  • Malware distribution: Redirect visitors to malicious downloads
  • Supply chain attack: If this changelog is displayed on a project's public site, every visitor is affected

The particularly insidious aspect is that commit messages are rarely scrutinized for security content—developers focus on code changes in pull requests, not the message text itself.

The Fix

The fix introduces HTML entity encoding for the five characters that enable HTML injection, applied to the msg variable before it's interpolated into innerHTML:

Before (Vulnerable)

changeLogContent.innerHTML += `
    <li class="text-white mb-2">
        ${formattedDate} | "${msg}"
    </li>
`;

After (Fixed)

const escapedMsg = msg.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
changeLogContent.innerHTML += `
    <li class="text-white mb-2">
        ${formattedDate} | "${escapedMsg}"
    </li>
`;

Why This Works

The encoding transforms dangerous characters into their HTML entity equivalents:

Character Encoded As Purpose
& &amp; Prevents entity injection
< &lt; Prevents tag opening
> &gt; Prevents tag closing
" &quot; Prevents attribute breakout
' &#039; Prevents attribute breakout (single quotes)

After encoding, the attacker's payload <img src=x onerror=alert(document.cookie)> becomes the harmless string &lt;img src=x onerror=alert(document.cookie)&gt;, which the browser renders as visible text rather than executable markup.

The order of replacements matters: & is replaced first to prevent double-encoding (e.g., &lt; becoming &amp;lt;).

Prevention & Best Practices

1. Prefer textContent Over innerHTML

When you only need to display text, use textContent or innerText:

const li = document.createElement('li');
li.className = 'text-white mb-2';
li.textContent = `${formattedDate} | "${msg}"`;
changeLogContent.appendChild(li);

This approach never parses HTML, eliminating XSS entirely for text-only content.

2. Use a Sanitization Library

For cases where you need to allow some HTML (e.g., markdown rendering), use a battle-tested library like DOMPurify:

import DOMPurify from 'dompurify';
changeLogContent.innerHTML += DOMPurify.sanitize(htmlContent);

3. Content Security Policy (CSP)

Deploy a strict CSP header as defense-in-depth:

Content-Security-Policy: default-src 'self'; script-src 'self'

This prevents inline script execution even if XSS encoding is bypassed.

4. Treat All External Data as Untrusted

API responses—even from your own services or GitHub—should always be treated as untrusted input. Network intermediaries, compromised APIs, or malicious contributors can inject payloads.

5. Automated Detection

Use static analysis tools that can trace data flow from API responses (sources) to DOM manipulation (sinks). Semgrep, ESLint security plugins, and dedicated SAST tools can flag innerHTML assignments with unencoded variables.

Key Takeaways

  • Never interpolate API response data into innerHTML without encoding—even "trusted" sources like GitHub's API can carry attacker-controlled content in fields like commit messages.
  • The msg variable in genChangeLogContent was a direct pipeline from attacker-controlled git history to every visitor's browser—a classic stored XSS vector.
  • HTML entity encoding of all five critical characters (& < > " ') is the minimum required defense when innerHTML must be used with dynamic data.
  • Commit messages are an overlooked attack surface—code review processes typically focus on diffs, not message content, making this a stealthy injection point.
  • The replacement order matters: always encode & first to prevent double-encoding issues with subsequent replacements.

How Orbis AppSec Detected This

  • Source: GitHub API response containing the commit message (msg variable from fetched commit data)
  • Sink: changeLogContent.innerHTML assignment in js/main.js:18 inside the genChangeLogContent function
  • Missing control: No HTML output encoding or sanitization between the API response data and the DOM insertion
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
  • Fix: Applied HTML entity encoding to the msg variable, replacing &, <, >, ", and ' with safe HTML entities before interpolation into innerHTML

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 vulnerability demonstrates how quickly a seemingly simple UI feature—displaying commit messages in a changelog—can become a critical security issue. The path from GitHub API response to innerHTML was completely unguarded, turning every commit message into a potential XSS payload delivery mechanism.

The fix is minimal but effective: five chained .replace() calls that neutralize HTML metacharacters before they reach the DOM. For developers building similar features, the lesson is clear: any data that originates outside your direct control—whether from APIs, databases, or user input—must be encoded for its output context before rendering.

References

Frequently Asked Questions

What is Cross-Site Scripting (XSS)?

XSS is a vulnerability where an attacker injects malicious scripts into web content that executes in other users' browsers, potentially stealing cookies, session tokens, or performing actions on behalf of the victim.

How do you prevent XSS in JavaScript?

Prevent XSS by encoding all untrusted data before inserting it into the DOM—use textContent instead of innerHTML, or apply HTML entity encoding to replace characters like <, >, &, ", and ' with their safe equivalents.

What CWE is Cross-Site Scripting?

Cross-Site Scripting is classified as CWE-79: Improper Neutralization of Input During Web Page Generation.

Is using innerHTML with template literals enough to prevent XSS?

No, template literals provide no sanitization. They simply interpolate values as-is, so any HTML or script tags in the interpolated string will be parsed and executed by the browser.

Can static analysis detect XSS?

Yes, static analysis tools can trace data flow from untrusted sources (like API responses) to dangerous sinks (like innerHTML) and flag missing sanitization, as demonstrated by the multi_agent_ai scanner that detected this issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #40

Related Articles

critical

How Cross-Site Scripting (XSS) happens in JavaScript template rendering and how to fix it

A cross-site scripting (XSS) vulnerability in `renderer/views/library.js` allowed attackers who could control mod metadata—such as category icons rendered in pack thumbnail grids—to inject arbitrary JavaScript through unescaped output in `innerHTML` assignments. The fix wraps the `catIcon()` return value in the existing `esc()` helper, ensuring all dynamically generated HTML content is properly encoded before insertion into the DOM.

critical

How XSS via unescaped sender_name happens in JavaScript chat widgets and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `frontend/scripts/chat-widget.js` allowed attackers to inject arbitrary JavaScript by crafting a malicious `sender_name` field, which was interpolated directly into a DOM template string without HTML encoding. The `renderFileContent()` function compounded the risk by also inserting unsanitized `file.name` and `file.url` values into `img`, `span`, and `anchor` elements. The fix applies `AppUtils.escapeHTML()` to every user-controlled value befo

critical

How HTML sanitizer bypass via XMP tag passthrough happens in Node.js and how to fix it

A critical cross-site scripting (XSS) vulnerability in the sanitize-html library allowed attackers to bypass HTML sanitization through improper handling of the `<xmp>` raw-text element. This vulnerability could enable stored XSS attacks in any Node.js application using sanitize-html versions prior to 2.17.4. The fix involved upgrading the library to properly handle raw-text elements and prevent script injection.

critical

How DOM-based XSS via jQuery .html() happens in JavaScript and how to fix it

A critical DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in CustomRankingInterface.js where user-imported JSON data containing malicious filter names could execute arbitrary JavaScript in victims' browsers. The fix replaces jQuery's unsafe `.html()` method with the safe `.text()` method, preventing script injection while preserving the intended functionality.

critical

How reflected XSS happens in Jinja2 template rendering and how to fix it

A reflected cross-site scripting (XSS) vulnerability was discovered in the similarity search HTML template where user input from the `query` form parameter was rendered directly into an HTML attribute without proper escaping. An attacker could inject malicious JavaScript by crafting a search query containing attribute-breaking payloads like `" onfocus="alert(document.cookie)" autofocus="`, which would execute in the victim's browser.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.