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

notice.html innerHTML Assignment in Browser Extension: CWE-79

A browser extension's content script fetched notice.html and assigned it directly to innerHTML, creating a DOM-based XSS vector if the extension package were compromised. The fix removes notice.html generation from the build process entirely and restructures the shadow DOM injection to use safe textContent assignment for styles only.

critical

fast-xml-parser 4.5.0 XSS: DOCTYPE Entity Injection in XML.parse()

fast-xml-parser versions 4.5.0 and earlier fail to sanitize DOCTYPE entity declarations during XML parsing, allowing attackers to inject JavaScript through crafted XML payloads. The vulnerability permits reflected XSS in applications that process untrusted XML input. Upgrading to version 5.7.0 eliminates the unsafe entity expansion path.

high

sanitizeBangumiHtml() XSS Fix: insertAdjacentHTML RCE via JSON

A high-severity cross-site scripting vulnerability existed where untrusted JSON data from bangumis.json was rendered directly into the DOM using insertAdjacentHTML without sanitization. An attacker who could modify this external data source could execute arbitrary JavaScript in visitors' browsers. The fix introduces a dedicated sanitizeBangumiHtml() function that strips script tags, event handlers, and javascript: URLs before insertion.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

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