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
Published September 6, 2026Reviewed 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.

Prevention & Best Practices

1. Use Established Libraries

Instead of writing custom sanitization, use well-tested libraries:
- DOMPurify for HTML sanitization
- he (HTML entities) for encoding/decoding
- Framework-provided utilities (React's JSX, Angular's built-in sanitization)

2. Context-Aware Encoding

Different contexts require different encoding:
- HTML body: Encode <, >, &
- HTML attributes: Also encode ", '
- JavaScript strings: Use \ escaping or JSON encoding
- URLs: Use encodeURIComponent()

3. Content Security Policy (CSP)

Implement CSP headers as a defense-in-depth measure:

Content-Security-Policy: default-src 'self'; script-src 'self'

4. Automated Security Testing

Integrate static analysis tools that can detect incomplete sanitization patterns. Tools like Semgrep can identify custom sanitization functions that miss important characters.

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.

References

Frequently Asked Questions

What is Cross-Site Scripting (XSS)?

XSS is a vulnerability where attackers inject malicious scripts into web pages viewed by other users, allowing them to steal cookies, session tokens, or perform actions on behalf of victims.

How do you prevent XSS in JavaScript?

Prevent XSS by encoding all user input before rendering it in HTML contexts, using functions that escape &, <, >, ", ', and backticks, or by using framework-provided sanitization libraries.

What CWE is XSS?

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

Is escaping angle brackets enough to prevent XSS?

No, escaping only angle brackets is insufficient. Attackers can use attribute injection with quotes (`"onmouseover=alert(1)"`), JavaScript context injection with single quotes, or template literal injection with backticks.

Can static analysis detect XSS?

Yes, static analysis tools can detect incomplete sanitization functions by tracking data flow from user input to HTML output and verifying that all dangerous characters are properly encoded.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #17

Related Articles

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.

critical

How Unsandboxed iframe Content Injection happens in JavaScript and how to fix it

A critical vulnerability in `app-viewer/js/LupineVault.js` allowed attacker-controlled HTML fetched from an external CDN to execute scripts in the application's full origin context by injecting it directly into an iframe's `srcdoc` attribute without any sandbox restrictions. The fix adds a `sandbox` attribute to the iframe element, restricting what the injected content can do even if it contains malicious scripts. This prevents cross-site scripting and origin-context script execution that could

critical

How Unsanitized External Content Injection happens in JavaScript and how to fix it

A critical content injection vulnerability in `app-viewer/js/youtube.js` allowed arbitrary HTML and JavaScript from a compromised external CDN to execute directly in the hosting origin's context. The fix replaces unsafe `fetch()`-then-inject patterns with direct URL assignment, eliminating the attack surface entirely. This change prevents supply-chain-style attacks where a compromised JSON manifest could deliver malicious payloads to every user of the viewer.

critical

How Unsafe Attribute Injection happens in JavaScript i18n and how to fix it

A critical attribute injection vulnerability in `assets/js/language.js` allowed attackers with write access to locale JSON files to inject arbitrary HTML attributes — including event handlers like `onclick` — into DOM elements via the `applyTranslations()` function. The fix introduces a strict allowlist (`SAFE_ATTRS`) that restricts which attributes the i18n system can set, closing the injection path entirely. This is a concrete reminder that any code path that writes attacker-influenced data in

critical

How Command Injection happens in Python subprocess calls and how to fix it

A critical command injection vulnerability was discovered in `spider/php/crawler.py` where the `PHPBridge.call()` method passed unvalidated external arguments directly to `subprocess.run()`. An attacker controlling the `spider_path` or `method` parameters could execute arbitrary PHP scripts or inject malicious method names. The fix adds strict input validation — requiring `method` to be a valid Python identifier and `spider_path` to resolve to an existing `.php` file — before any subprocess exec