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:
-
An attacker with write access to the repository pushes a commit with this message:
<img src=x onerror=alert(document.cookie)> -
When any user visits the page that renders the changelog, the
genChangeLogContentfunction fetches commits from the GitHub API. -
The malicious commit message is interpolated directly into
innerHTML. -
The browser parses the
<img>tag, fails to load thesrc=ximage, and fires theonerrorhandler—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.cookieexfiltration 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
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 |
|---|---|---|
& |
& |
Prevents entity injection |
< |
< |
Prevents tag opening |
> |
> |
Prevents tag closing |
" |
" |
Prevents attribute breakout |
' |
' |
Prevents attribute breakout (single quotes) |
After encoding, the attacker's payload <img src=x onerror=alert(document.cookie)> becomes the harmless string <img src=x onerror=alert(document.cookie)>, 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., < becoming &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
innerHTMLwithout encoding—even "trusted" sources like GitHub's API can carry attacker-controlled content in fields like commit messages. - The
msgvariable ingenChangeLogContentwas 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 wheninnerHTMLmust 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 (
msgvariable from fetched commit data) - Sink:
changeLogContent.innerHTMLassignment injs/main.js:18inside thegenChangeLogContentfunction - 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
msgvariable, replacing&,<,>,", and'with safe HTML entities before interpolation intoinnerHTML
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.