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

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

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

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.

high

How DOM-based Cross-Site Scripting happens in JavaScript and how to fix it

A high-severity DOM-based XSS vulnerability in `public/audio_match_demo/index.html` allowed attackers to inject malicious JavaScript through manipulated song metadata from API responses. The fix replaces dangerous HTML string concatenation with secure DOM API methods that automatically escape content.

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.