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(/</g, '<').replace(/>/g, '>').replace(/</g, '<').replace(/>/g, '>');
}
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(/</g, '<').replace(/>/g, '>').replace(/</g, '<').replace(/>/g, '>');
}
After (Secure)
function sanitizeInput(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`');
}
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 <, and then the & in < would become &lt;—which is incorrect.
What Each Encoding Prevents
| Character | Entity | Attack Vector Blocked |
|---|---|---|
& |
& |
Entity manipulation, double-encoding issues |
< |
< |
Opening HTML tags, script injection |
> |
> |
Closing HTML tags |
" |
" |
Breaking out of double-quoted attributes |
' |
' |
Breaking out of single-quoted attributes/strings |
` |
` |
Template literal injection |
The fix also removes the confusing reverse-encoding logic (/</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.