Back to Blog
critical SEVERITY5 min read

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.

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

Answer Summary

This is a Cross-Site Scripting (XSS) vulnerability (CWE-79) in JavaScript caused by an incomplete `sanitizeInput()` function that only escaped angle brackets (`<` and `>`) but ignored quotes, ampersands, and backticks. Attackers could bypass this sanitization using payloads like `onmouseover=alert(1)` or template literal injection `${alert(1)}`. The fix adds proper HTML entity encoding for all six dangerous characters: `&`, `<`, `>`, `"`, `'`, and backticks.

Vulnerability at a Glance

cweCWE-79
fixExtended encoding to cover &, <, >, ", ', and backtick characters
riskAttackers can execute arbitrary JavaScript in users' browsers, stealing sessions or data
languageJavaScript
root causesanitizeInput() only escaped angle brackets, leaving quotes and backticks unprotected
vulnerabilityCross-Site Scripting (XSS) via Incomplete Input Sanitization

Introduction

In script.js, we discovered a critical XSS vulnerability lurking in what appeared to be a security function. The sanitizeInput() function at line 823 was designed to prevent cross-site scripting attacks, but its implementation left a dangerous gap: it only escaped angle brackets while ignoring five other characters that attackers routinely exploit.

The vulnerable code looked deceptively secure:

function sanitizeInput(str) {
  return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

This matters because any developer who sees a function named sanitizeInput would reasonably assume it provides complete protection. In reality, this function was a false sense of security—a partially locked door that attackers could walk right through.

The Vulnerability Explained

The sanitizeInput() function's fatal flaw was its narrow focus on angle brackets. While preventing <script> tag injection is important, modern XSS attacks have evolved far beyond simple script tags.

What Was Missing

The original function failed to encode:
- Ampersands (&) - Can break existing HTML entities and enable entity-based attacks
- Double quotes (") - Allow breaking out of HTML attributes
- Single quotes (') - Enable JavaScript string escaping and attribute injection
- Backticks (`) - Permit template literal injection in modern JavaScript

Real Attack Scenarios Against This Code

Scenario 1: Attribute Context Injection

If user input is placed inside an HTML attribute:

<div title="USER_INPUT_HERE">

An attacker could submit: " onmouseover="alert(document.cookie)

The sanitized output would be:

<div title="" onmouseover="alert(document.cookie)">

The double quote was never escaped, allowing the attacker to close the attribute and inject an event handler—no angle brackets required.

Scenario 2: Template Literal Exploitation

In JavaScript template literals:

const message = `Welcome, ${sanitizeInput(username)}!`;

An attacker could submit: ${alert(1)}

Since backticks weren't escaped, the payload executes directly within the template literal context.

Scenario 3: JavaScript String Context

If sanitized input is placed in a JavaScript string:

var name = 'USER_INPUT_HERE';

An attacker could submit: '; alert(1); //

The unescaped single quote breaks out of the string, allowing arbitrary code execution.

Why This Is Critical

This vulnerability affects any part of the application that relies on sanitizeInput() to protect against XSS. Since the function is in script.js—a core file handling user interactions—the attack surface is potentially extensive. An attacker could steal session cookies, redirect users to malicious sites, or perform actions on behalf of authenticated users.

The Fix

The fix transforms the incomplete sanitization into comprehensive HTML entity encoding by addressing all six dangerous characters in the correct order.

Before (Vulnerable)

function sanitizeInput(str) {
  return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

After (Secure)

function sanitizeInput(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
    .replace(/`/g, '&#96;');
}

Why This Order Matters

The ampersand (&) is encoded first for a critical reason: if you encode other characters first and then encode ampersands, you'll double-encode. For example, < becomes &lt;, and then the & in &lt; would become &amp;lt;—which is incorrect.

What Each Encoding Prevents

Character Entity Attack Vector Blocked
& &amp; Entity manipulation, double-encoding issues
< &lt; Opening HTML tags, script injection
> &gt; Closing HTML tags
" &quot; Breaking out of double-quoted attributes
' &#39; Breaking out of single-quoted attributes/strings
` &#96; Template literal injection

The fix also removes the confusing reverse-encoding logic (/&lt;/g, '<') that was in the original function, which could have introduced additional vulnerabilities by decoding previously-encoded content.

Key Takeaways

  • Never assume angle bracket escaping alone prevents XSS—quotes, backticks, and ampersands are equally dangerous in different contexts
  • The sanitizeInput() function name implied security it didn't provide—always verify sanitization functions encode all context-relevant characters
  • Encoding order matters—ampersands must be encoded first to prevent double-encoding
  • Custom sanitization functions are risky—prefer established libraries like DOMPurify that have been battle-tested
  • Template literals introduced new attack vectors—backtick encoding is now essential in modern JavaScript applications

How Orbis AppSec Detected This

  • Source: User-controlled input passed to sanitizeInput() function
  • Sink: HTML output contexts throughout the application where sanitized strings are rendered
  • Missing control: Incomplete character encoding—only < and > were escaped while &, ", ', and backticks were passed through unmodified
  • CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
  • Fix: Extended the sanitizeInput() function to encode all six XSS-relevant characters in the correct order

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 a common but dangerous pattern: security functions that look correct but provide incomplete protection. The original sanitizeInput() function would pass a casual code review—it clearly attempts to prevent XSS by encoding angle brackets. But security requires completeness, and the missing character encodings left the door wide open for sophisticated attackers.

When implementing input sanitization, always consider all the contexts where that input might be used. A string that's safe in an HTML body might be dangerous in an attribute, a JavaScript string, or a template literal. When in doubt, use established libraries that have already solved these problems, and implement defense-in-depth with Content Security Policy headers.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #17

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.